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.
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/PKG-INFO +2 -1
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/__init__.py +1 -1
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_lrucache.py +7 -3
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/asynctools.py +15 -5
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/contextlib.py +3 -4
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/contextlib.pyi +6 -3
- asyncstdlib-3.12.5/asyncstdlib/functools.py +316 -0
- asyncstdlib-3.12.5/asyncstdlib/functools.pyi +40 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/itertools.py +126 -56
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/itertools.pyi +9 -7
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/pyproject.toml +1 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_functools.py +60 -12
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_functools_lru.py +42 -19
- asyncstdlib-3.12.3/asyncstdlib/functools.py +0 -201
- asyncstdlib-3.12.3/asyncstdlib/functools.pyi +0 -26
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/LICENSE +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/README.rst +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_core.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_lrucache.pyi +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_typing.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/_utility.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/builtins.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/builtins.pyi +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/heapq.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/heapq.pyi +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/asyncstdlib/py.typed +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/__init__.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_asynctools.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_builtins.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_contextlib.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_heapq.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_helpers.py +0 -0
- {asyncstdlib-3.12.3 → asyncstdlib-3.12.5}/unittests/test_itertools.py +0 -0
- {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
|
+
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"
|
|
@@ -28,7 +28,9 @@ from ._typing import Protocol, TypedDict, AC
|
|
|
28
28
|
from ._utility import public_module
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
@public_module(
|
|
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(
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
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) ->
|
|
243
|
-
return
|
|
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
|
-
) ->
|
|
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__(
|
|
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
|
-
) ->
|
|
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) ->
|
|
339
|
-
return
|
|
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[
|
|
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) ->
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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[
|
|
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) ->
|
|
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
|
-
|
|
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[
|
|
231
|
-
) -> AsyncIterator[tuple[
|
|
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[
|
|
235
|
-
|
|
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
|
|
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
|
|
28
|
+
assert type(Pair.total) is CachedProperty
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@sync
|
|
31
32
|
async def test_cache_property_nodict():
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
with pytest.raises(Exception): # noqa: B017
|
|
33
|
+
class Foo:
|
|
34
|
+
__slots__ = ()
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
def __init__(self):
|
|
37
|
+
pass # pragma: no cover
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
@a.cached_property
|
|
40
|
+
async def bar(self):
|
|
41
|
+
pass # pragma: no cover
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
count: int = 0
|
|
42
51
|
|
|
43
52
|
class Counter:
|
|
53
|
+
kind = staticmethod
|
|
54
|
+
|
|
44
55
|
def __init__(self):
|
|
45
|
-
nonlocal
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
return
|
|
62
|
+
nonlocal count
|
|
63
|
+
count += 1
|
|
64
|
+
return count
|
|
54
65
|
|
|
55
66
|
return Counter
|
|
56
67
|
|
|
57
68
|
|
|
58
|
-
counter_factories
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
99
|
-
|
|
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
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|