Skip to content

Commit 4b15040

Browse files
Maintenance Cleanup 2021/07 (#53)
* constrained decorator type * core slots * mypy is mandatory * no mypy for pypy :( * PEP 561 -- Distributing and Packaging Type Information * tee cleans up like any other iterator * restricted python versions * overload for sorted * moved all typing to separate module
1 parent 47919db commit 4b15040

File tree

13 files changed

+288
-180
lines changed

13 files changed

+288
-180
lines changed

.github/workflows/verification.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ jobs:
2121
- name: Format with black
2222
run: |
2323
black asyncstdlib unittests --diff --check
24+
- name: Verify with MyPy
25+
run: |
26+
mypy --pretty

asyncstdlib/_core.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from inspect import iscoroutinefunction
22
from typing import (
3-
TypeVar,
43
AsyncIterator,
54
Iterable,
65
AsyncIterable,
@@ -11,25 +10,21 @@
1110
Callable,
1211
)
1312

14-
T = TypeVar("T")
15-
16-
17-
AnyIterable = Union[Iterable[T], AsyncIterable[T]]
13+
from ._typing import T, AnyIterable
1814

1915

2016
class Sentinel:
2117
"""Placeholder with configurable ``repr``"""
2218

19+
__slots__ = ("name",)
20+
2321
def __init__(self, name):
2422
self.name = name
2523

2624
def __repr__(self):
2725
return self.name
2826

2927

30-
__ITER_SENTINEL = Sentinel("<no default>")
31-
32-
3328
def aiter(subject: AnyIterable[T]) -> AsyncIterator[T]:
3429
"""
3530
An async iterator object yielding elements from ``subject``
@@ -58,19 +53,23 @@ async def _aiter_sync(iterable: Iterable[T]) -> AsyncIterator[T]:
5853
class ScopedIter(Generic[T]):
5954
"""Context manager that provides and cleans up an iterator for an iterable"""
6055

56+
__slots__ = ("_iterable", "_iterator")
57+
6158
def __init__(self, iterable: AnyIterable[T]):
62-
self._iterable = iterable
59+
self._iterable: Optional[AnyIterable[T]] = iterable
6360
self._iterator: Optional[AsyncIterator[T]] = None
6461

6562
async def __aenter__(self) -> AsyncIterator[T]:
66-
assert self._iterator is None, f"{self.__class__.__name__} is not re-entrant"
63+
assert (
64+
self._iterable is not None
65+
), f"{self.__class__.__name__} is not re-entrant"
6766
self._iterator = aiter(self._iterable)
6867
self._iterable = None
6968
return self._iterator
7069

71-
async def __aexit__(self, exc_type, exc_val, exc_tb):
70+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool:
7271
try:
73-
aclose = self._iterator.aclose()
72+
aclose = self._iterator.aclose() # type: ignore
7473
except AttributeError:
7574
pass
7675
else:
@@ -89,7 +88,7 @@ def awaitify(
8988
) -> Callable[..., Awaitable[T]]:
9089
"""Ensure that ``function`` can be used in ``await`` expressions"""
9190
if iscoroutinefunction(function):
92-
return function
91+
return function # type: ignore
9392
else:
9493
return Awaitify(function)
9594

@@ -108,10 +107,10 @@ def __call__(self, *args, **kwargs) -> Awaitable[T]:
108107
if async_call is None:
109108
value = self.__wrapped__(*args, **kwargs)
110109
if isinstance(value, Awaitable):
111-
self._async_call = self.__wrapped__
110+
self._async_call = self.__wrapped__ # type: ignore
112111
return value
113112
else:
114-
self._async_call = force_async(self.__wrapped__)
113+
self._async_call = force_async(self.__wrapped__) # type: ignore
115114
return await_value(value)
116115
else:
117116
return async_call(*args, **kwargs)

asyncstdlib/_lrucache.py

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,19 @@
88
from typing import (
99
NamedTuple,
1010
Callable,
11-
Awaitable,
12-
TypeVar,
11+
Any,
1312
Optional,
1413
Tuple,
1514
Dict,
1615
Union,
1716
)
1817
from functools import update_wrapper
1918
from collections import OrderedDict
20-
import sys
21-
22-
if sys.version_info[:2] >= (3, 8):
23-
from typing import Protocol, TypedDict
24-
else:
25-
from typing_extensions import Protocol, TypedDict
2619

20+
from ._typing import Protocol, TypedDict, C
2721
from ._utility import public_module
2822

2923

30-
R = TypeVar("R")
31-
32-
3324
@public_module("asyncstdlib.functools")
3425
class CacheInfo(NamedTuple):
3526
"""
@@ -65,16 +56,16 @@ class CacheParameters(TypedDict):
6556

6657

6758
@public_module("asyncstdlib.functools")
68-
class LRUAsyncCallable(Protocol[R]):
59+
class LRUAsyncCallable(Protocol[C]):
6960
"""
7061
:py:class:`~typing.Protocol` of a LRU cache wrapping a callable to an awaitable
7162
"""
7263

7364
#: The callable wrapped by this cache
74-
__wrapped__: Callable[..., Awaitable[R]]
65+
__wrapped__: C
7566

76-
async def __call__(self, *args, **kwargs) -> R:
77-
"""Get the result of ``await __wrapped__(...)`` from the cache or evaluation"""
67+
#: Get the result of ``await __wrapped__(...)`` from the cache or evaluation
68+
__call__: C
7869

7970
def cache_parameters(self) -> CacheParameters:
8071
"""Get the parameters of the cache"""
@@ -90,7 +81,7 @@ def cache_clear(self) -> None:
9081

9182

9283
@public_module("asyncstdlib.functools")
93-
def lru_cache(maxsize: Optional[int] = 128, typed: bool = False):
84+
def lru_cache(maxsize: Optional[Union[int, Callable]] = 128, typed: bool = False):
9485
"""
9586
Least Recently Used cache for async functions
9687
@@ -143,7 +134,8 @@ def lru_cache(maxsize: Optional[int] = 128, typed: bool = False):
143134
"first argument to 'lru_cache' must be an int, a callable or None"
144135
)
145136

146-
def lru_decorator(function: Callable[..., Awaitable[R]]) -> LRUAsyncCallable[R]:
137+
def lru_decorator(function: C) -> LRUAsyncCallable[C]:
138+
assert not callable(maxsize)
147139
if maxsize is None:
148140
wrapper = _unbound_lru(function=function, typed=typed)
149141
elif maxsize == 0:
@@ -190,14 +182,12 @@ def from_call(
190182
return cls(key)
191183

192184

193-
def _empty_lru(
194-
function: Callable[..., Awaitable[R]], typed: bool
195-
) -> LRUAsyncCallable[R]:
185+
def _empty_lru(function: C, typed: bool) -> LRUAsyncCallable[C]:
196186
"""Wrap the async ``function`` in an async LRU cache without any capacity"""
197187
# cache statistics
198188
misses = 0
199189

200-
async def wrapper(*args, **kwargs) -> R:
190+
async def wrapper(*args, **kwargs):
201191
nonlocal misses
202192
misses += 1
203193
return await function(*args, **kwargs)
@@ -212,25 +202,23 @@ def cache_clear():
212202
nonlocal misses
213203
misses = 0
214204

215-
wrapper.cache_parameters = cache_parameters
216-
wrapper.cache_info = cache_info
217-
wrapper.cache_clear = cache_clear
218-
return wrapper
205+
wrapper.cache_parameters = cache_parameters # type: ignore
206+
wrapper.cache_info = cache_info # type: ignore
207+
wrapper.cache_clear = cache_clear # type: ignore
208+
return wrapper # type: ignore
219209

220210

221-
def _unbound_lru(
222-
function: Callable[..., Awaitable[R]], typed: bool
223-
) -> LRUAsyncCallable[R]:
211+
def _unbound_lru(function: C, typed: bool) -> LRUAsyncCallable[C]:
224212
"""Wrap the async ``function`` in an async LRU cache with infinite capacity"""
225213
# local lookup
226214
make_key = CallKey.from_call
227215
# cache statistics
228216
hits = 0
229217
misses = 0
230218
# cache content
231-
cache: Dict[Union[CallKey, int, str], R] = {}
219+
cache: Dict[Union[CallKey, int, str], Any] = {}
232220

233-
async def wrapper(*args, **kwargs) -> R:
221+
async def wrapper(*args, **kwargs):
234222
nonlocal hits, misses
235223
key = make_key(args, kwargs, typed=typed)
236224
try:
@@ -259,26 +247,24 @@ def cache_clear():
259247
hits = 0
260248
cache.clear()
261249

262-
wrapper.cache_parameters = cache_parameters
263-
wrapper.cache_info = cache_info
264-
wrapper.cache_clear = cache_clear
265-
return wrapper
250+
wrapper.cache_parameters = cache_parameters # type: ignore
251+
wrapper.cache_info = cache_info # type: ignore
252+
wrapper.cache_clear = cache_clear # type: ignore
253+
return wrapper # type: ignore
266254

267255

268-
def _bounded_lru(
269-
function: Callable[..., Awaitable[R]], typed: bool, maxsize: int
270-
) -> LRUAsyncCallable[R]:
256+
def _bounded_lru(function: C, typed: bool, maxsize: int) -> LRUAsyncCallable[C]:
271257
"""Wrap the async ``function`` in an async LRU cache with fixed capacity"""
272258
# local lookup
273259
make_key = CallKey.from_call
274260
# cache statistics
275261
hits = 0
276262
misses = 0
277263
# cache content
278-
cache: OrderedDict[Union[int, str, CallKey], R] = OrderedDict()
264+
cache: OrderedDict[Union[int, str, CallKey], Any] = OrderedDict()
279265
filled = False
280266

281-
async def wrapper(*args, **kwargs) -> R:
267+
async def wrapper(*args, **kwargs):
282268
nonlocal hits, misses, filled
283269
key = make_key(args, kwargs, typed=typed)
284270
try:
@@ -319,7 +305,7 @@ def cache_clear():
319305
filled = False
320306
cache.clear()
321307

322-
wrapper.cache_parameters = cache_parameters
323-
wrapper.cache_info = cache_info
324-
wrapper.cache_clear = cache_clear
325-
return wrapper
308+
wrapper.cache_parameters = cache_parameters # type: ignore
309+
wrapper.cache_info = cache_info # type: ignore
310+
wrapper.cache_clear = cache_clear # type: ignore
311+
return wrapper # type: ignore

asyncstdlib/_typing.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
Helper module to simplify version specific typing imports
3+
4+
This module is for internal use only. Do *not* put any new
5+
"async typing" definitions here.
6+
"""
7+
import sys
8+
from typing import TypeVar, Hashable, Union, AsyncIterable, Iterable, Callable
9+
10+
if sys.version_info[:2] >= (3, 8):
11+
from typing import Protocol, AsyncContextManager, ContextManager, TypedDict
12+
else:
13+
from typing_extensions import (
14+
Protocol,
15+
AsyncContextManager,
16+
ContextManager,
17+
TypedDict,
18+
)
19+
20+
__all__ = [
21+
"Protocol",
22+
"AsyncContextManager",
23+
"ContextManager",
24+
"TypedDict",
25+
"T",
26+
"T1",
27+
"T2",
28+
"T3",
29+
"T4",
30+
"T5",
31+
"R",
32+
"C",
33+
"HK",
34+
"LT",
35+
"ADD",
36+
"AnyIterable",
37+
]
38+
39+
# TypeVars for argument/return type
40+
T = TypeVar("T")
41+
T1 = TypeVar("T1")
42+
T2 = TypeVar("T2")
43+
T3 = TypeVar("T3")
44+
T4 = TypeVar("T4")
45+
T5 = TypeVar("T5")
46+
R = TypeVar("R", covariant=True)
47+
C = TypeVar("C", bound=Callable)
48+
49+
#: Hashable Key
50+
HK = TypeVar("HK", bound=Hashable)
51+
52+
# LT < LT
53+
LT = TypeVar("LT", bound="SupportsLT")
54+
55+
56+
class SupportsLT(Protocol):
57+
def __lt__(self: LT, other: LT) -> bool:
58+
raise NotImplementedError
59+
60+
61+
# ADD + ADD
62+
ADD = TypeVar("ADD", bound="SupportsAdd")
63+
64+
65+
class SupportsAdd(Protocol):
66+
def __add__(self: ADD, other: ADD) -> bool:
67+
raise NotImplementedError
68+
69+
70+
#: (async) iter T
71+
AnyIterable = Union[Iterable[T], AsyncIterable[T]]

asyncstdlib/_utility.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
from typing import TypeVar, Any
1+
from typing import TypeVar, Any, Optional
22

3-
T = TypeVar("T")
3+
from ._typing import Protocol
44

55

6-
def public_module(module_name: str, qual_name: str = None):
6+
class Definition(Protocol):
7+
"""
8+
Type of objects created from a class or function definition
9+
"""
10+
11+
__name__: str
12+
__module__: str
13+
__qualname__: str
14+
15+
16+
D = TypeVar("D", bound=Definition)
17+
18+
19+
def public_module(module_name: str, qual_name: Optional[str] = None):
720
"""Set the module name of a function or class"""
821

9-
def decorator(thing: T) -> T:
22+
def decorator(thing: D) -> D:
1023
thing.__module__ = module_name
1124
if qual_name is not None:
1225
thing.__qualname__ = qual_name

0 commit comments

Comments
 (0)