asyncstdlib 3.12.3__tar.gz → 3.12.5__tar.gz

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.
Files changed (34) hide show
  1. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/PKG-INFO +2 -1
  2. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/__init__.py +1 -1
  3. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_lrucache.py +7 -3
  4. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/asynctools.py +15 -5
  5. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/contextlib.py +3 -4
  6. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/contextlib.pyi +6 -3
  7. asyncstdlib-3.12.5/asyncstdlib/functools.py +316 -0
  8. asyncstdlib-3.12.5/asyncstdlib/functools.pyi +40 -0
  9. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/itertools.py +126 -56
  10. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/itertools.pyi +9 -7
  11. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/pyproject.toml +1 -0
  12. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_functools.py +60 -12
  13. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_functools_lru.py +42 -19
  14. asyncstdlib-3.12.3/asyncstdlib/functools.py +0 -201
  15. asyncstdlib-3.12.3/asyncstdlib/functools.pyi +0 -26
  16. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/LICENSE +0 -0
  17. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/README.rst +0 -0
  18. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_core.py +0 -0
  19. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_lrucache.pyi +0 -0
  20. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_typing.py +0 -0
  21. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_utility.py +0 -0
  22. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/builtins.py +0 -0
  23. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/builtins.pyi +0 -0
  24. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/heapq.py +0 -0
  25. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/heapq.pyi +0 -0
  26. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/py.typed +0 -0
  27. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/__init__.py +0 -0
  28. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_asynctools.py +0 -0
  29. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_builtins.py +0 -0
  30. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_contextlib.py +0 -0
  31. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_heapq.py +0 -0
  32. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_helpers.py +0 -0
  33. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_itertools.py +0 -0
  34. {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: asyncstdlib
3
- Version: 3.12.3
3
+ Version: 3.12.5
4
4
  Summary: The missing async toolbox
5
5
  Keywords: async,enumerate,itertools,builtins,functools,contextlib
6
6
  Author-email: Max Fischer <maxfischer2781@gmail.com>
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Requires-Dist: sphinx ; extra == "doc"
20
21
  Requires-Dist: sphinxcontrib-trio ; extra == "doc"
21
22
  Requires-Dist: pytest ; extra == "test"
@@ -45,7 +45,7 @@ from .itertools import (
45
45
  from .asynctools import borrow, scoped_iter, await_each, any_iter, apply, sync
46
46
  from .heapq import merge, nlargest, nsmallest
47
47
 
48
- __version__ = "3.12.3"
48
+ __version__ = "3.12.5"
49
49
 
50
50
  __all__ = [
51
51
  "anext",
@@ -28,7 +28,9 @@ from ._typing import Protocol, TypedDict, AC
28
28
  from ._utility import public_module
29
29
 
30
30
 
31
- @public_module("asyncstdlib.functools")
31
+ @public_module(
32
+ "asyncstdlib.functools"
33
+ ) # pyright: ignore[reportArgumentType,reportUntypedClassDecorator]
32
34
  class CacheInfo(NamedTuple):
33
35
  """
34
36
  Metadata on the current state of a cache
@@ -81,7 +83,9 @@ class LRUAsyncCallable(Protocol[AC]):
81
83
  """Descriptor ``__get__`` for caches to bind them on lookup"""
82
84
  if instance is None:
83
85
  return self
84
- return LRUAsyncBoundCallable(self, instance)
86
+ return LRUAsyncBoundCallable(
87
+ self, instance
88
+ ) # pyright: ignore[reportUnknownVariableType]
85
89
 
86
90
  #: Get the result of ``await __wrapped__(...)`` from the cache or evaluation
87
91
  __call__: AC
@@ -120,7 +124,7 @@ class LRUAsyncCallable(Protocol[AC]):
120
124
  # these are fake and only exist for placeholders
121
125
  S = TypeVar("S")
122
126
  S2 = TypeVar("S2")
123
- P = TypeVar("P")
127
+ P = TypeVar("P") # actually a ParamSpec, see .pyi
124
128
  R = TypeVar("R")
125
129
 
126
130
 
@@ -10,6 +10,7 @@ from typing import (
10
10
  Awaitable,
11
11
  AsyncIterable,
12
12
  Callable,
13
+ Coroutine,
13
14
  Any,
14
15
  overload,
15
16
  Optional,
@@ -76,7 +77,7 @@ class _BorrowedAsyncIterator(AsyncGenerator[T, S]):
76
77
  if hasattr(self, "athrow"):
77
78
  self.athrow = wrapper_iterator.athrow
78
79
 
79
- def aclose(self) -> Awaitable[None]:
80
+ def aclose(self) -> Coroutine[Any, Any, None]:
80
81
  return self._aclose_wrapper()
81
82
 
82
83
 
@@ -110,16 +111,25 @@ class _ScopedAsyncIteratorContext(AsyncContextManager[AsyncIterator[T]]):
110
111
  self._borrowed_iter = _ScopedAsyncIterator(self._iterator)
111
112
  return self._borrowed_iter
112
113
 
113
- async def __aexit__(self, *args: Any) -> bool:
114
+ async def __aexit__(self, *args: Any) -> None:
114
115
  await self._borrowed_iter._aclose_wrapper() # type: ignore
115
116
  await self._iterator.aclose() # type: ignore
116
- return False
117
117
 
118
118
  def __repr__(self) -> str:
119
119
  return f"<{self.__class__.__name__} of {self._iterator!r} at 0x{(id(self)):x}>"
120
120
 
121
121
 
122
- def borrow(iterator: AsyncIterator[T], /) -> AsyncIterator[T]:
122
+ @overload
123
+ def borrow(iterator: AsyncGenerator[T, S], /) -> AsyncGenerator[T, S]: ...
124
+
125
+
126
+ @overload
127
+ def borrow(iterator: AsyncIterator[T], /) -> AsyncIterator[T]: ...
128
+
129
+
130
+ def borrow(
131
+ iterator: Union[AsyncIterator[T], AsyncGenerator[T, Any]], /
132
+ ) -> Union[AsyncIterator[T], AsyncGenerator[T, Any]]:
123
133
  """
124
134
  Borrow an async iterator, preventing to ``aclose`` it
125
135
 
@@ -142,7 +152,7 @@ def borrow(iterator: AsyncIterator[T], /) -> AsyncIterator[T]:
142
152
  "borrowing requires an async iterator "
143
153
  + f"with __aiter__ and __anext__ method, got {type(iterator).__name__}"
144
154
  )
145
- return _BorrowedAsyncIterator(iterator)
155
+ return _BorrowedAsyncIterator[T, Any](iterator)
146
156
 
147
157
 
148
158
  def scoped_iter(iterable: AnyIterable[T], /) -> AsyncContextManager[AsyncIterator[T]]:
@@ -199,9 +199,8 @@ class Closing(Generic[AClose]):
199
199
  async def __aenter__(self) -> AClose:
200
200
  return self.thing
201
201
 
202
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
202
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
203
203
  await self.thing.aclose()
204
- return False
205
204
 
206
205
 
207
206
  closing = Closing
@@ -239,8 +238,8 @@ class NullContext(AsyncContextManager[T]):
239
238
  async def __aenter__(self) -> T:
240
239
  return self.enter_result
241
240
 
242
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
243
- return False
241
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
242
+ return None
244
243
 
245
244
 
246
245
  nullcontext = NullContext
@@ -66,7 +66,7 @@ class closing(Generic[AClose]):
66
66
  exc_type: type[BaseException] | None,
67
67
  exc_val: BaseException | None,
68
68
  exc_tb: TracebackType | None,
69
- ) -> bool: ...
69
+ ) -> None: ...
70
70
 
71
71
  class nullcontext(AsyncContextManager[T]):
72
72
  enter_result: T
@@ -74,14 +74,17 @@ class nullcontext(AsyncContextManager[T]):
74
74
  @overload
75
75
  def __init__(self: nullcontext[None], enter_result: None = ...) -> None: ...
76
76
  @overload
77
- def __init__(self: nullcontext[T], enter_result: T) -> None: ...
77
+ def __init__(
78
+ self: nullcontext[T], # pyright: ignore[reportInvalidTypeVarUse]
79
+ enter_result: T,
80
+ ) -> None: ...
78
81
  async def __aenter__(self: nullcontext[T]) -> T: ...
79
82
  async def __aexit__(
80
83
  self,
81
84
  exc_type: type[BaseException] | None,
82
85
  exc_val: BaseException | None,
83
86
  exc_tb: TracebackType | None,
84
- ) -> bool: ...
87
+ ) -> None: ...
85
88
 
86
89
  SE = TypeVar(
87
90
  "SE",
@@ -0,0 +1,316 @@
1
+ from asyncio import iscoroutinefunction
2
+ from typing import (
3
+ Callable,
4
+ Awaitable,
5
+ Union,
6
+ Any,
7
+ Generic,
8
+ Generator,
9
+ Optional,
10
+ Coroutine,
11
+ AsyncContextManager,
12
+ Type,
13
+ cast,
14
+ )
15
+
16
+ from ._typing import T, AC, AnyIterable, R
17
+ from ._core import ScopedIter, awaitify as _awaitify, Sentinel
18
+ from .builtins import anext
19
+ from .contextlib import nullcontext
20
+
21
+ from ._lrucache import (
22
+ lru_cache,
23
+ CacheInfo,
24
+ CacheParameters,
25
+ LRUAsyncCallable,
26
+ LRUAsyncBoundCallable,
27
+ )
28
+
29
+ __all__ = [
30
+ "cache",
31
+ "lru_cache",
32
+ "CacheInfo",
33
+ "CacheParameters",
34
+ "LRUAsyncCallable",
35
+ "LRUAsyncBoundCallable",
36
+ "reduce",
37
+ "cached_property",
38
+ "CachedProperty",
39
+ ]
40
+
41
+
42
+ def cache(user_function: AC) -> LRUAsyncCallable[AC]:
43
+ """
44
+ Simple unbounded cache, aka memoization, for async functions
45
+
46
+ This is a convenience function, equivalent to :py:func:`~.lru_cache`
47
+ with a ``maxsize`` of :py:data:`None`.
48
+ """
49
+ return lru_cache(maxsize=None)(user_function)
50
+
51
+
52
+ class AwaitableValue(Generic[R]):
53
+ """Helper to provide an arbitrary value in ``await``"""
54
+
55
+ __slots__ = ("value",)
56
+
57
+ def __init__(self, value: R):
58
+ self.value = value
59
+
60
+ # noinspection PyUnreachableCode
61
+ def __await__(self) -> Generator[None, None, R]:
62
+ return self.value
63
+ yield # type: ignore # pragma: no cover
64
+
65
+ def __repr__(self) -> str:
66
+ return f"{self.__class__.__name__}({self.value!r})"
67
+
68
+
69
+ class _FutureCachedValue(Generic[R, T]):
70
+ """A placeholder object to control concurrent access to a cached awaitable value.
71
+
72
+ When given a lock to coordinate access, only the first task to await on a
73
+ cached property triggers the underlying coroutine. Once a value has been
74
+ produced, all tasks are unblocked and given the same, single value.
75
+
76
+ """
77
+
78
+ __slots__ = ("_get_attribute", "_instance", "_name", "_lock")
79
+
80
+ def __init__(
81
+ self,
82
+ get_attribute: Callable[[T], Coroutine[Any, Any, R]],
83
+ instance: T,
84
+ name: str,
85
+ lock: AsyncContextManager[Any],
86
+ ):
87
+ self._get_attribute = get_attribute
88
+ self._instance = instance
89
+ self._name = name
90
+ self._lock = lock
91
+
92
+ def __await__(self) -> Generator[None, None, R]:
93
+ return self._await_impl().__await__()
94
+
95
+ @property
96
+ def _instance_value(self) -> Awaitable[R]:
97
+ """Retrieve whatever is currently cached on the instance
98
+
99
+ If the instance (no longer) has this attribute, it was deleted and the
100
+ process is restarted by delegating to the descriptor.
101
+
102
+ """
103
+ try:
104
+ return self._instance.__dict__[self._name]
105
+ except KeyError:
106
+ # something deleted the cached value or future cached value placeholder. Restart
107
+ # the fetch by delegating to the cached_property descriptor.
108
+ return getattr(self._instance, self._name)
109
+
110
+ async def _await_impl(self) -> R:
111
+ if (stored := self._instance_value) is self:
112
+ # attempt to get the lock
113
+ async with self._lock:
114
+ # check again for a cached value
115
+ if (stored := self._instance_value) is self:
116
+ # the instance attribute is still this placeholder, and we
117
+ # hold the lock. Start the getter to store the value on the
118
+ # instance and return the value.
119
+ return await self._get_attribute(self._instance)
120
+
121
+ # another task produced a value, or the instance.__dict__ object was
122
+ # deleted in the interim.
123
+ return await stored
124
+
125
+ def __repr__(self) -> str:
126
+ return (
127
+ f"<{type(self).__name__} for '{type(self._instance).__name__}."
128
+ f"{self._name}' at {id(self):#x}>"
129
+ )
130
+
131
+
132
+ class CachedProperty(Generic[T, R]):
133
+ def __init__(
134
+ self,
135
+ getter: Callable[[T], Awaitable[R]],
136
+ asynccontextmanager_type: Type[AsyncContextManager[Any]] = nullcontext,
137
+ ):
138
+ self.func = getter
139
+ self.attrname = None
140
+ self.__doc__ = getter.__doc__
141
+ self._asynccontextmanager_type = asynccontextmanager_type
142
+
143
+ def __set_name__(self, owner: Any, name: str) -> None:
144
+ if self.attrname is None:
145
+ self.attrname = name
146
+ elif name != self.attrname:
147
+ raise TypeError(
148
+ "Cannot assign the same cached_property to two different names "
149
+ f"({self.attrname!r} and {name!r})."
150
+ )
151
+
152
+ def __get__(
153
+ self, instance: Optional[T], owner: Optional[Type[Any]]
154
+ ) -> Union["CachedProperty[T, R]", Awaitable[R]]:
155
+ if instance is None:
156
+ return self
157
+
158
+ name = self.attrname
159
+ if name is None:
160
+ raise TypeError(
161
+ "Cannot use cached_property instance without calling __set_name__ on it."
162
+ )
163
+
164
+ # check for write access first; not all objects have __dict__ (e.g. class defines slots)
165
+ try:
166
+ cache = instance.__dict__
167
+ except AttributeError:
168
+ msg = (
169
+ f"No '__dict__' attribute on {type(instance).__name__!r} "
170
+ f"instance to cache {name!r} property."
171
+ )
172
+ raise TypeError(msg) from None
173
+
174
+ # store a placeholder for other tasks to access the future cached value
175
+ # on this instance. It takes care of coordinating between different
176
+ # tasks awaiting on the placeholder until the cached value has been
177
+ # produced.
178
+ wrapper = _FutureCachedValue(
179
+ self._get_attribute, instance, name, self._asynccontextmanager_type()
180
+ )
181
+ cache[name] = wrapper
182
+ return wrapper
183
+
184
+ async def _get_attribute(self, instance: T) -> R:
185
+ value = await self.func(instance)
186
+ name = self.attrname
187
+ assert name is not None # enforced in __get__
188
+ instance.__dict__[name] = AwaitableValue(value)
189
+ return value
190
+
191
+
192
+ def cached_property(
193
+ type_or_getter: Union[Type[AsyncContextManager[Any]], Callable[[T], Awaitable[R]]],
194
+ /,
195
+ ) -> Union[
196
+ Callable[[Callable[[T], Awaitable[R]]], CachedProperty[T, R]],
197
+ CachedProperty[T, R],
198
+ ]:
199
+ """
200
+ Transform a method into an attribute whose value is cached
201
+
202
+ When applied to an asynchronous method of a class, instances have an attribute
203
+ of the same name as the method (similar to :py:class:`property`). Using this
204
+ attribute with ``await`` provides the value of using the method with ``await``.
205
+
206
+ The attribute value is cached on the instance after being computed;
207
+ subsequent uses of the attribute with ``await`` provide the cached value,
208
+ without executing the method again.
209
+ The cached value can be cleared using ``del``, in which case the next
210
+ access will recompute the value using the wrapped method.
211
+
212
+ .. code-block:: python3
213
+
214
+ import asyncstdlib as a
215
+
216
+ class Resource:
217
+ def __init__(self, url):
218
+ self.url = url
219
+
220
+ @a.cached_property
221
+ async def data(self):
222
+ return await asynclib.get(self.url)
223
+
224
+ resource = Resource("http://example.com")
225
+ print(await resource.data) # needs some time...
226
+ print(await resource.data) # finishes instantly
227
+ del resource.data
228
+ print(await resource.data) # needs some time...
229
+
230
+ Unlike a :py:class:`property`, this type does not support
231
+ :py:meth:`~property.setter` or :py:meth:`~property.deleter`.
232
+
233
+ If the attribute is accessed by multiple tasks before a cached value has
234
+ been produced, the getter can be run more than once. The final cached value
235
+ is determined by the last getter coroutine to return. To enforce that the
236
+ getter is executed at most once, provide an appropriate lock type - e.g. the
237
+ :py:class:`asyncio.Lock` class in an :py:mod:`asyncio` application - and
238
+ access is automatically synchronised.
239
+
240
+ .. code-block:: python3
241
+
242
+ from asyncio import Lock, gather
243
+
244
+ class Resource:
245
+ def __init__(self, url):
246
+ self.url = url
247
+
248
+ @a.cached_property(Lock)
249
+ async def data(self):
250
+ return await asynclib.get(self.url)
251
+
252
+ resource = Resource("http://example.com")
253
+ print(*(await gather(resource.data, resource.data)))
254
+
255
+ .. note::
256
+
257
+ Instances on which a value is to be cached must have a
258
+ ``__dict__`` attribute that is a mutable mapping.
259
+ """
260
+ if isinstance(type_or_getter, type) and issubclass(
261
+ type_or_getter, AsyncContextManager
262
+ ):
263
+
264
+ def decorator(
265
+ coroutine: Callable[[T], Awaitable[R]],
266
+ ) -> CachedProperty[T, R]:
267
+ return CachedProperty(
268
+ coroutine,
269
+ asynccontextmanager_type=cast(
270
+ Type[AsyncContextManager[Any]], type_or_getter
271
+ ),
272
+ )
273
+
274
+ return decorator
275
+
276
+ if not iscoroutinefunction(type_or_getter):
277
+ raise ValueError("cached_property can only be used with a coroutine function")
278
+
279
+ return CachedProperty(type_or_getter)
280
+
281
+
282
+ __REDUCE_SENTINEL = Sentinel("<no default>")
283
+
284
+
285
+ async def reduce(
286
+ function: Union[Callable[[T, T], T], Callable[[T, T], Awaitable[T]]],
287
+ iterable: AnyIterable[T],
288
+ initial: T = __REDUCE_SENTINEL, # type: ignore
289
+ ) -> T:
290
+ """
291
+ Reduce an (async) iterable by cumulative application of an (async) function
292
+
293
+ :raises TypeError: if ``iterable`` is empty and ``initial`` is not given
294
+
295
+ Applies the ``function`` from the beginning of ``iterable``, as if executing
296
+ ``await function(current, anext(iterable))`` until ``iterable`` is exhausted.
297
+ Note that the output of ``function`` should be valid as its first input.
298
+
299
+ The optional ``initial`` is prepended to all items of ``iterable``
300
+ when applying ``function``. If the combination of ``initial``
301
+ and ``iterable`` contains exactly one item, it is returned without
302
+ calling ``function``.
303
+ """
304
+ async with ScopedIter(iterable) as item_iter:
305
+ try:
306
+ value = (
307
+ initial if initial is not __REDUCE_SENTINEL else await anext(item_iter)
308
+ )
309
+ except StopAsyncIteration:
310
+ raise TypeError(
311
+ "reduce() of empty sequence with no initial value"
312
+ ) from None
313
+ function = _awaitify(function)
314
+ async for head in item_iter:
315
+ value = await function(value, head)
316
+ return value
@@ -0,0 +1,40 @@
1
+ from typing import Any, AsyncContextManager, Awaitable, Callable, Generic, overload
2
+
3
+ from ._typing import T, T1, T2, AC, AnyIterable, R
4
+
5
+ from ._lrucache import (
6
+ LRUAsyncCallable as LRUAsyncCallable,
7
+ LRUAsyncBoundCallable as LRUAsyncBoundCallable,
8
+ lru_cache as lru_cache,
9
+ )
10
+
11
+ def cache(user_function: AC) -> LRUAsyncCallable[AC]: ...
12
+
13
+ class CachedProperty(Generic[T, R]):
14
+ def __init__(
15
+ self,
16
+ getter: Callable[[T], Awaitable[R]],
17
+ lock_type: type[AsyncContextManager[Any]] = ...,
18
+ ) -> None: ...
19
+ def __set_name__(self, owner: Any, name: str) -> None: ...
20
+ @overload
21
+ def __get__(self, instance: None, owner: type[Any]) -> "CachedProperty[T, R]": ...
22
+ @overload
23
+ def __get__(self, instance: T, owner: type | None) -> Awaitable[R]: ...
24
+ # __set__ is not defined at runtime, but you are allowed to replace the cached value
25
+ def __set__(self, instance: T, value: R) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
26
+ # __del__ is not defined at runtime, but you are allowed to delete the cached value
27
+ def __del__(self, instance: T) -> None: ...
28
+
29
+ @overload
30
+ def cached_property(getter: Callable[[T], Awaitable[R]], /) -> CachedProperty[T, R]: ...
31
+ @overload
32
+ def cached_property(
33
+ asynccontextmanager_type: type[AsyncContextManager[Any]], /
34
+ ) -> Callable[[Callable[[T], Awaitable[R]]], CachedProperty[T, R]]: ...
35
+ @overload
36
+ async def reduce(
37
+ function: Callable[[T1, T2], T1], iterable: AnyIterable[T2], initial: T1
38
+ ) -> T1: ...
39
+ @overload
40
+ async def reduce(function: Callable[[T, T], T], iterable: AnyIterable[T]) -> T: ...
@@ -13,12 +13,13 @@ from typing import (
13
13
  Iterable,
14
14
  Iterator,
15
15
  Tuple,
16
+ cast,
16
17
  overload,
17
18
  AsyncGenerator,
18
19
  )
19
20
  from collections import deque
20
21
 
21
- from ._typing import ACloseable, T, AnyIterable, ADD
22
+ from ._typing import ACloseable, R, T, AnyIterable, ADD
22
23
  from ._utility import public_module
23
24
  from ._core import (
24
25
  ScopedIter,
@@ -35,6 +36,7 @@ from .builtins import (
35
36
  )
36
37
 
37
38
  S = TypeVar("S")
39
+ T_co = TypeVar("T_co", covariant=True)
38
40
 
39
41
 
40
42
  async def cycle(iterable: AnyIterable[T]) -> AsyncIterator[T]:
@@ -335,8 +337,8 @@ class NoLock:
335
337
  async def __aenter__(self) -> None:
336
338
  pass
337
339
 
338
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
339
- return False
340
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
341
+ return None
340
342
 
341
343
 
342
344
  async def tee_peer(
@@ -454,15 +456,14 @@ class Tee(Generic[T]):
454
456
  ) -> Union[AsyncIterator[T], Tuple[AsyncIterator[T], ...]]:
455
457
  return self._children[item]
456
458
 
457
- def __iter__(self) -> Iterator[AnyIterable[T]]:
459
+ def __iter__(self) -> Iterator[AsyncIterator[T]]:
458
460
  yield from self._children
459
461
 
460
462
  async def __aenter__(self) -> "Tee[T]":
461
463
  return self
462
464
 
463
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
465
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
464
466
  await self.aclose()
465
- return False
466
467
 
467
468
  async def aclose(self) -> None:
468
469
  for child in self._children:
@@ -543,12 +544,86 @@ async def identity(x: T) -> T:
543
544
  return x
544
545
 
545
546
 
546
- async def groupby(
547
- iterable: AnyIterable[Any],
548
- key: Optional[
549
- Union[Callable[[Any], Any], Callable[[Any], Awaitable[Any]]]
550
- ] = identity,
551
- ) -> AsyncIterator[Tuple[Any, AsyncIterator[Any]]]:
547
+ class _GroupByState(Generic[R, T_co]):
548
+ """Internal state for the groupby iterator, shared between the parent and groups"""
549
+
550
+ __slots__ = (
551
+ "_iterator",
552
+ "_key_func",
553
+ "_current_value",
554
+ "target_key",
555
+ "current_key",
556
+ "current_group",
557
+ )
558
+
559
+ _sentinel = cast(T_co, object())
560
+
561
+ def __init__(
562
+ self, iterator: AsyncIterator[T_co], key_func: Callable[[T_co], Awaitable[R]]
563
+ ):
564
+ self._iterator = iterator
565
+ self._key_func = key_func
566
+ self._current_value = self._sentinel
567
+
568
+ async def step(self) -> None:
569
+ # can raise StopAsyncIteration
570
+ value = await anext(self._iterator)
571
+ key = await self._key_func(value)
572
+ self._current_value, self.current_key = value, key
573
+
574
+ async def maybe_step(self) -> None:
575
+ """Only step if there is no current value"""
576
+ if self._current_value is self._sentinel:
577
+ await self.step()
578
+
579
+ def consume_value(self) -> T_co:
580
+ """Return the current value after removing it from the current state"""
581
+ value, self._current_value = self._current_value, self._sentinel
582
+ return value
583
+
584
+ async def aclose(self) -> None:
585
+ """Close the underlying iterator"""
586
+ if (group := self.current_group) is not None:
587
+ await group.aclose()
588
+ if isinstance(self._iterator, ACloseable):
589
+ await self._iterator.aclose()
590
+
591
+
592
+ class _Grouper(AsyncIterator[T_co], Generic[R, T_co]):
593
+ """A single group iterator, part of a series of groups yielded by groupby"""
594
+
595
+ __slots__ = ("_target_key", "_state")
596
+
597
+ def __init__(self, target_key: R, state: "_GroupByState[R, T_co]") -> None:
598
+ self._target_key = target_key
599
+ self._state = state
600
+
601
+ async def __anext__(self) -> T_co:
602
+ state = self._state
603
+ # the groupby already advanced to another group
604
+ if state.current_group is not self:
605
+ raise StopAsyncIteration
606
+ await state.maybe_step()
607
+ # the step advanced the iterator to another group
608
+ if self._target_key != state.current_key:
609
+ raise StopAsyncIteration
610
+ return state.consume_value()
611
+
612
+ async def aclose(self) -> None:
613
+ """
614
+ Close the group iterator
615
+
616
+ Note: this does _not_ close the underlying groupby managed iterator;
617
+ closing a single group shouldn't affect other groups in the series.
618
+ """
619
+ state = self._state
620
+ if state.current_group is not self:
621
+ return
622
+ state.current_group = None
623
+
624
+
625
+ @public_module(__name__, "groupby")
626
+ class GroupBy(AsyncIterator[Tuple[R, AsyncIterator[T_co]]], Generic[R, T_co]):
552
627
  """
553
628
  Create an async iterator over consecutive keys and groups from the (async) iterable
554
629
 
@@ -568,49 +643,44 @@ async def groupby(
568
643
  required up-front for sorting, this loses the advantage of asynchronous,
569
644
  lazy iteration and evaluation.
570
645
  """
571
- # whether the current group was exhausted and the next begins already
572
- exhausted = False
573
- # `current_*`: buffer for key/value the current group peeked beyond its end
574
- current_key = current_value = nothing = object()
575
- make_key: Callable[[Any], Awaitable[Any]] = (
576
- _awaitify(key) if key is not None else identity # type: ignore
577
- )
578
- async with ScopedIter(iterable) as async_iter:
579
- # fast-forward mode: advance to the next group
580
- async def seek_group() -> AsyncIterator[Any]:
581
- nonlocal current_value, current_key, exhausted
582
- # Note: `value` always ends up being some T
583
- # - value is something: we can never unset it
584
- # - value is `nothing`: the previous group was not exhausted,
585
- # and we scan at least one new value
586
- value: Any = current_value
587
- if not exhausted:
588
- previous_key = current_key
589
- while previous_key == current_key:
590
- value = await anext(async_iter)
591
- current_key = await make_key(value)
592
- current_value = nothing
593
- exhausted = False
594
- return group(current_key, value=value)
595
-
596
- # the lazy iterable of all items with the same key
597
- async def group(desired_key: Any, value: Any) -> AsyncIterator[Any]:
598
- nonlocal current_value, current_key, exhausted
599
- yield value
600
- async for value in async_iter:
601
- next_key: Any = await make_key(value)
602
- if next_key == desired_key:
603
- yield value
604
- else:
605
- exhausted = True
606
- current_value = value
607
- current_key = next_key
608
- break
609
646
 
647
+ __slots__ = ("_state",)
648
+
649
+ def __init__(
650
+ self,
651
+ iterable: AnyIterable[T_co],
652
+ key: Optional[
653
+ Union[Callable[[T_co], R], Callable[[T_co], Awaitable[R]]]
654
+ ] = None,
655
+ ):
656
+ key_func = (
657
+ cast(Callable[[T_co], Awaitable[R]], identity)
658
+ if key is None
659
+ else _awaitify(key)
660
+ )
661
+ self._state = _GroupByState(aiter(iterable), key_func)
662
+
663
+ async def __anext__(self) -> Tuple[R, AsyncIterator[T_co]]:
664
+ state = self._state
665
+ # already disable the current group to avoid concurrency issues
666
+ state.current_group = None
667
+ await state.maybe_step()
610
668
  try:
611
- while True:
612
- next_group = await seek_group()
613
- async with ScopedIter(next_group) as scoped_group:
614
- yield current_key, scoped_group
615
- except StopAsyncIteration:
616
- return
669
+ target_key = state.target_key
670
+ except AttributeError:
671
+ # no target key yet, skip scanning
672
+ pass
673
+ else:
674
+ # scan to the next group
675
+ while state.current_key == target_key:
676
+ await state.step()
677
+
678
+ state.target_key = current_key = state.current_key
679
+ state.current_group = group = _Grouper(current_key, state)
680
+ return (current_key, group)
681
+
682
+ async def aclose(self) -> None:
683
+ await self._state.aclose()
684
+
685
+
686
+ groupby = GroupBy
@@ -130,9 +130,9 @@ class tee(Generic[T]):
130
130
  def __getitem__(self, item: int) -> AsyncIterator[T]: ...
131
131
  @overload
132
132
  def __getitem__(self, item: slice) -> tuple[AsyncIterator[T], ...]: ...
133
- def __iter__(self) -> Iterator[AnyIterable[T]]: ...
133
+ def __iter__(self) -> Iterator[AsyncIterator[T]]: ...
134
134
  async def __aenter__(self: Self) -> Self: ...
135
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ...
135
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
136
136
  async def aclose(self) -> None: ...
137
137
 
138
138
  def pairwise(iterable: AnyIterable[T]) -> AsyncIterator[tuple[T, T]]: ...
@@ -223,13 +223,15 @@ def zip_longest(
223
223
  fillvalue: F,
224
224
  ) -> AsyncIterator[tuple[T | F, ...]]: ...
225
225
 
226
- K = TypeVar("K")
226
+ K_co = TypeVar("K_co", covariant=True)
227
+ T_co = TypeVar("T_co", covariant=True)
227
228
 
228
229
  @overload
229
230
  def groupby(
230
- iterable: AnyIterable[T], key: None = ...
231
- ) -> AsyncIterator[tuple[T, AsyncIterator[T]]]: ...
231
+ iterable: AnyIterable[T_co], key: None = ...
232
+ ) -> AsyncIterator[tuple[T_co, AsyncIterator[T_co]]]: ...
232
233
  @overload
233
234
  def groupby(
234
- iterable: AnyIterable[T], key: Callable[[T], Awaitable[K]] | Callable[[T], K]
235
- ) -> AsyncIterator[tuple[K, AsyncIterator[T]]]: ...
235
+ iterable: AnyIterable[T_co],
236
+ key: Callable[[T_co], Awaitable[K_co]] | Callable[[T], K_co],
237
+ ) -> AsyncIterator[tuple[K_co, AsyncIterator[T_co]]]: ...
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.10",
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
23
24
  ]
24
25
  license = {"file" = "LICENSE"}
25
26
  keywords = ["async", "enumerate", "itertools", "builtins", "functools", "contextlib"]
@@ -3,8 +3,9 @@ import functools
3
3
  import pytest
4
4
 
5
5
  import asyncstdlib as a
6
+ from asyncstdlib.functools import CachedProperty
6
7
 
7
- from .utility import sync, asyncify, multi_sync, Switch, Schedule
8
+ from .utility import Lock, Schedule, Switch, asyncify, multi_sync, sync
8
9
 
9
10
 
10
11
  @sync
@@ -24,24 +25,23 @@ async def test_cached_property():
24
25
  assert (await pair.total) == 3
25
26
  del pair.total
26
27
  assert (await pair.total) == 4
27
- assert type(Pair.total) is a.cached_property
28
+ assert type(Pair.total) is CachedProperty
28
29
 
29
30
 
30
31
  @sync
31
32
  async def test_cache_property_nodict():
32
- # note: The exact error is version- and possibly implementation-dependent.
33
- # Some Python version wrap all errors from __set_name__.
34
- with pytest.raises(Exception): # noqa: B017
33
+ class Foo:
34
+ __slots__ = ()
35
35
 
36
- class Pair:
37
- __slots__ = "a", "b"
36
+ def __init__(self):
37
+ pass # pragma: no cover
38
38
 
39
- def __init__(self, a, b):
40
- pass # pragma: no cover
39
+ @a.cached_property
40
+ async def bar(self):
41
+ pass # pragma: no cover
41
42
 
42
- @a.cached_property
43
- async def total(self):
44
- pass # pragma: no cover
43
+ with pytest.raises(TypeError):
44
+ Foo().bar
45
45
 
46
46
 
47
47
  @multi_sync
@@ -66,6 +66,54 @@ async def test_cache_property_order():
66
66
  assert (await val.cached) == 1337 # last value fetched
67
67
 
68
68
 
69
+ @multi_sync
70
+ async def test_cache_property_lock_order():
71
+ class Value:
72
+ def __init__(self, value):
73
+ self.value = value
74
+
75
+ @a.cached_property(Lock)
76
+ async def cached(self):
77
+ value = self.value
78
+ await Switch()
79
+ return value
80
+
81
+ async def check_cached(to, expected):
82
+ val.value = to
83
+ assert (await val.cached) == expected
84
+
85
+ val = Value(0)
86
+ await Schedule(check_cached(5, 5), check_cached(12, 5), check_cached(1337, 5))
87
+ assert (await val.cached) == 5 # first value fetched
88
+
89
+
90
+ @multi_sync
91
+ async def test_cache_property_lock_deletion():
92
+ class Value:
93
+ def __init__(self, value):
94
+ self.value = value
95
+
96
+ @a.cached_property(Lock)
97
+ async def cached(self):
98
+ value = self.value
99
+ await Switch()
100
+ return value
101
+
102
+ async def check_cached(to, expected):
103
+ val.value = to
104
+ assert (await val.cached) == expected
105
+
106
+ async def delete_attribute(to):
107
+ val.value = to
108
+ awaitable = val.cached
109
+ del val.cached
110
+ assert (await awaitable) == to
111
+
112
+ val = Value(0)
113
+ await Schedule(check_cached(5, 5), delete_attribute(12), check_cached(1337, 12))
114
+ assert (await val.cached) == 12 # first value fetch after deletion
115
+
116
+
69
117
  @sync
70
118
  async def test_reduce():
71
119
  async def reduction(x, y):
@@ -1,3 +1,4 @@
1
+ from typing import Callable, Any
1
2
  import sys
2
3
 
3
4
  import pytest
@@ -7,8 +8,15 @@ import asyncstdlib as a
7
8
  from .utility import sync
8
9
 
9
10
 
10
- def method_counter(size):
11
+ class Counter:
12
+ kind: object
13
+ count: Any
14
+
15
+
16
+ def method_counter(size: "int | None") -> "type[Counter]":
11
17
  class Counter:
18
+ kind = None
19
+
12
20
  def __init__(self):
13
21
  self._count = 0
14
22
 
@@ -20,9 +28,10 @@ def method_counter(size):
20
28
  return Counter
21
29
 
22
30
 
23
- def classmethod_counter(size):
31
+ def classmethod_counter(size: "int | None") -> "type[Counter]":
24
32
  class Counter:
25
33
  _count = 0
34
+ kind = classmethod
26
35
 
27
36
  def __init__(self):
28
37
  type(self)._count = 0
@@ -36,32 +45,40 @@ def classmethod_counter(size):
36
45
  return Counter
37
46
 
38
47
 
39
- def staticmethod_counter(size):
48
+ def staticmethod_counter(size: "int | None") -> "type[Counter]":
40
49
  # I'm sorry for writing this test – please don't do this at home!
41
- _count = 0
50
+ count: int = 0
42
51
 
43
52
  class Counter:
53
+ kind = staticmethod
54
+
44
55
  def __init__(self):
45
- nonlocal _count
46
- _count = 0
56
+ nonlocal count
57
+ count = 0
47
58
 
48
59
  @staticmethod
49
60
  @a.lru_cache(maxsize=size)
50
61
  async def count():
51
- nonlocal _count
52
- _count += 1
53
- return _count
62
+ nonlocal count
63
+ count += 1
64
+ return count
54
65
 
55
66
  return Counter
56
67
 
57
68
 
58
- counter_factories = [method_counter, classmethod_counter, staticmethod_counter]
69
+ counter_factories: "list[Callable[[int | None], type[Counter]]]" = [
70
+ method_counter,
71
+ classmethod_counter,
72
+ staticmethod_counter,
73
+ ]
59
74
 
60
75
 
61
76
  @pytest.mark.parametrize("size", [0, 3, 10, None])
62
77
  @pytest.mark.parametrize("counter_factory", counter_factories)
63
78
  @sync
64
- async def test_method_plain(size, counter_factory):
79
+ async def test_method_plain(
80
+ size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
81
+ ):
65
82
  """Test caching without resetting"""
66
83
 
67
84
  counter_type = counter_factory(size)
@@ -76,7 +93,9 @@ async def test_method_plain(size, counter_factory):
76
93
  @pytest.mark.parametrize("size", [0, 3, 10, None])
77
94
  @pytest.mark.parametrize("counter_factory", counter_factories)
78
95
  @sync
79
- async def test_method_clear(size, counter_factory):
96
+ async def test_method_clear(
97
+ size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
98
+ ):
80
99
  """Test caching with resetting everything"""
81
100
  counter_type = counter_factory(size)
82
101
  for _instance in range(4):
@@ -91,14 +110,16 @@ async def test_method_clear(size, counter_factory):
91
110
  @pytest.mark.parametrize("size", [0, 3, 10, None])
92
111
  @pytest.mark.parametrize("counter_factory", counter_factories)
93
112
  @sync
94
- async def test_method_discard(size, counter_factory):
113
+ async def test_method_discard(
114
+ size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
115
+ ):
95
116
  """Test caching with resetting specific item"""
96
117
  counter_type = counter_factory(size)
97
- if (
98
- sys.version_info < (3, 9)
99
- and type(counter_type.__dict__["count"]) is classmethod
118
+ if not (
119
+ (3, 9) <= sys.version_info[:2] <= (3, 12)
120
+ or counter_type.kind is not classmethod
100
121
  ):
101
- pytest.skip("classmethod does not respect descriptors up to 3.8")
122
+ pytest.skip("classmethod only respects descriptors between 3.9 and 3.12")
102
123
  for _instance in range(4):
103
124
  instance = counter_type()
104
125
  for reset in range(5):
@@ -111,7 +132,9 @@ async def test_method_discard(size, counter_factory):
111
132
  @pytest.mark.parametrize("size", [0, 3, 10, None])
112
133
  @pytest.mark.parametrize("counter_factory", counter_factories)
113
134
  @sync
114
- async def test_method_metadata(size, counter_factory):
135
+ async def test_method_metadata(
136
+ size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
137
+ ):
115
138
  """Test cache metadata on methods"""
116
139
  tp = counter_factory(size)
117
140
  for instance in range(4):
@@ -133,7 +156,7 @@ async def test_method_metadata(size, counter_factory):
133
156
 
134
157
 
135
158
  @pytest.mark.parametrize("size", [None, 0, 10, 128])
136
- def test_wrapper_attributes(size):
159
+ def test_wrapper_attributes(size: "int | None"):
137
160
  class Bar:
138
161
  @a.lru_cache
139
162
  async def method(self, int_arg: int):
@@ -1,201 +0,0 @@
1
- from typing import (
2
- Callable,
3
- Awaitable,
4
- Union,
5
- Any,
6
- Generic,
7
- Generator,
8
- Optional,
9
- Coroutine,
10
- overload,
11
- )
12
-
13
- from ._typing import T, AC, AnyIterable
14
- from ._core import ScopedIter, awaitify as _awaitify, Sentinel
15
- from .builtins import anext
16
- from ._utility import public_module
17
-
18
- from ._lrucache import (
19
- lru_cache,
20
- CacheInfo,
21
- CacheParameters,
22
- LRUAsyncCallable,
23
- LRUAsyncBoundCallable,
24
- )
25
-
26
- __all__ = [
27
- "cache",
28
- "lru_cache",
29
- "CacheInfo",
30
- "CacheParameters",
31
- "LRUAsyncCallable",
32
- "LRUAsyncBoundCallable",
33
- "reduce",
34
- "cached_property",
35
- ]
36
-
37
-
38
- def cache(user_function: AC) -> LRUAsyncCallable[AC]:
39
- """
40
- Simple unbounded cache, aka memoization, for async functions
41
-
42
- This is a convenience function, equivalent to :py:func:`~.lru_cache`
43
- with a ``maxsize`` of :py:data:`None`.
44
- """
45
- return lru_cache(maxsize=None)(user_function)
46
-
47
-
48
- class AwaitableValue(Generic[T]):
49
- """Helper to provide an arbitrary value in ``await``"""
50
-
51
- __slots__ = ("value",)
52
-
53
- def __init__(self, value: T):
54
- self.value = value
55
-
56
- # noinspection PyUnreachableCode
57
- def __await__(self) -> Generator[None, None, T]:
58
- return self.value
59
- yield # type: ignore # pragma: no cover
60
-
61
- def __repr__(self) -> str:
62
- return f"{self.__class__.__name__}({self.value!r})"
63
-
64
-
65
- class _RepeatableCoroutine(Generic[T]):
66
- """Helper to ``await`` a coroutine also more or less than just once"""
67
-
68
- __slots__ = ("call", "args", "kwargs")
69
-
70
- def __init__(
71
- self, __call: Callable[..., Coroutine[Any, Any, T]], *args: Any, **kwargs: Any
72
- ):
73
- self.call = __call
74
- self.args = args
75
- self.kwargs = kwargs
76
-
77
- def __await__(self) -> Generator[Any, Any, T]:
78
- return self.call(*self.args, **self.kwargs).__await__()
79
-
80
- def __repr__(self) -> str:
81
- return f"<{self.__class__.__name__} object {self.call.__name__} at {id(self)}>"
82
-
83
-
84
- @public_module(__name__, "cached_property")
85
- class CachedProperty(Generic[T]):
86
- """
87
- Transform a method into an attribute whose value is cached
88
-
89
- When applied to an asynchronous method of a class, instances have an attribute
90
- of the same name as the method (similar to :py:class:`property`). Using this
91
- attribute with ``await`` provides the value of using the method with ``await``.
92
-
93
- The attribute value is cached on the instance after being computed;
94
- subsequent uses of the attribute with ``await`` provide the cached value,
95
- without executing the method again.
96
- The cached value can be cleared using ``del``, in which case the next
97
- access will recompute the value using the wrapped method.
98
-
99
- .. code-block:: python3
100
-
101
- import asyncstdlib as a
102
-
103
- class Resource:
104
- def __init__(self, url):
105
- self.url = url
106
-
107
- @a.cached_property
108
- async def data(self):
109
- return await asynclib.get(self.url)
110
-
111
- resource = Resource(1, 3)
112
- print(await resource.data) # needs some time...
113
- print(await resource.data) # finishes instantly
114
- del resource.data
115
- print(await resource.data) # needs some time...
116
-
117
- Unlike a :py:class:`property`, this type does not support
118
- :py:meth:`~property.setter` or :py:meth:`~property.deleter`.
119
-
120
- .. note::
121
-
122
- Instances on which a value is to be cached must have a
123
- ``__dict__`` attribute that is a mutable mapping.
124
- """
125
-
126
- def __init__(self, getter: Callable[[Any], Awaitable[T]]):
127
- self.__wrapped__ = getter
128
- self._name = getter.__name__
129
- self.__doc__ = getter.__doc__
130
-
131
- def __set_name__(self, owner: Any, name: str) -> None:
132
- # Check whether we can store anything on the instance
133
- # Note that this is a failsafe, and might fail ugly.
134
- # People who are clever enough to avoid this heuristic
135
- # should also be clever enough to know the why and what.
136
- if not any("__dict__" in dir(cls) for cls in owner.__mro__):
137
- raise TypeError(
138
- "'cached_property' requires '__dict__' "
139
- f"on {owner.__name__!r} to store {name}"
140
- )
141
- self._name = name
142
-
143
- @overload
144
- def __get__(self, instance: None, owner: type) -> "CachedProperty[T]": ...
145
-
146
- @overload
147
- def __get__(self, instance: object, owner: Optional[type]) -> Awaitable[T]: ...
148
-
149
- def __get__(
150
- self, instance: Optional[object], owner: Optional[type]
151
- ) -> Union["CachedProperty[T]", Awaitable[T]]:
152
- if instance is None:
153
- return self
154
- # __get__ may be called multiple times before it is first awaited to completion
155
- # provide a placeholder that acts just like the final value does
156
- return _RepeatableCoroutine(self._get_attribute, instance)
157
-
158
- async def _get_attribute(self, instance: object) -> T:
159
- value = await self.__wrapped__(instance)
160
- instance.__dict__[self._name] = AwaitableValue(value)
161
- return value
162
-
163
-
164
- cached_property = CachedProperty
165
-
166
-
167
- __REDUCE_SENTINEL = Sentinel("<no default>")
168
-
169
-
170
- async def reduce(
171
- function: Union[Callable[[T, T], T], Callable[[T, T], Awaitable[T]]],
172
- iterable: AnyIterable[T],
173
- initial: T = __REDUCE_SENTINEL, # type: ignore
174
- ) -> T:
175
- """
176
- Reduce an (async) iterable by cumulative application of an (async) function
177
-
178
- :raises TypeError: if ``iterable`` is empty and ``initial`` is not given
179
-
180
- Applies the ``function`` from the beginning of ``iterable``, as if executing
181
- ``await function(current, anext(iterable))`` until ``iterable`` is exhausted.
182
- Note that the output of ``function`` should be valid as its first input.
183
-
184
- The optional ``initial`` is prepended to all items of ``iterable``
185
- when applying ``function``. If the combination of ``initial``
186
- and ``iterable`` contains exactly one item, it is returned without
187
- calling ``function``.
188
- """
189
- async with ScopedIter(iterable) as item_iter:
190
- try:
191
- value = (
192
- initial if initial is not __REDUCE_SENTINEL else await anext(item_iter)
193
- )
194
- except StopAsyncIteration:
195
- raise TypeError(
196
- "reduce() of empty sequence with no initial value"
197
- ) from None
198
- function = _awaitify(function)
199
- async for head in item_iter:
200
- value = await function(value, head)
201
- return value
@@ -1,26 +0,0 @@
1
- from typing import Any, Awaitable, Callable, Generic, overload
2
-
3
- from ._typing import T, T1, T2, AC, AnyIterable
4
-
5
- from ._lrucache import (
6
- LRUAsyncCallable as LRUAsyncCallable,
7
- LRUAsyncBoundCallable as LRUAsyncBoundCallable,
8
- lru_cache as lru_cache,
9
- )
10
-
11
- def cache(user_function: AC) -> LRUAsyncCallable[AC]: ...
12
-
13
- class cached_property(Generic[T]):
14
- def __init__(self, getter: Callable[[Any], Awaitable[T]]) -> None: ...
15
- def __set_name__(self, owner: Any, name: str) -> None: ...
16
- @overload
17
- def __get__(self, instance: None, owner: type) -> "cached_property[T]": ...
18
- @overload
19
- def __get__(self, instance: object, owner: type | None) -> Awaitable[T]: ...
20
-
21
- @overload
22
- async def reduce(
23
- function: Callable[[T1, T2], T1], iterable: AnyIterable[T2], initial: T1
24
- ) -> T1: ...
25
- @overload
26
- async def reduce(function: Callable[[T, T], T], iterable: AnyIterable[T]) -> T: ...
File without changes
File without changes