Skip to content

Commit a6fc497

Browse files
Type hinting for lru_cache on methods (#105)
* added unittest to verify lru_cache works on methods * added type tests * added stub for lrucache * preserve cache on type lookup * documented type testing scheme * ignore type tests for coverage and pytest
1 parent b966a95 commit a6fc497

File tree

6 files changed

+156
-1
lines changed

6 files changed

+156
-1
lines changed

.codecov.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ignore:
2+
# type tests are not execute, so there is no code coverage
3+
- "typetests"

asyncstdlib/_lrucache.pyi

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from ._typing import AC, Protocol, R as R, TypedDict
2+
from typing import (
3+
Any,
4+
Awaitable,
5+
Callable,
6+
NamedTuple,
7+
Optional,
8+
overload,
9+
)
10+
11+
class CacheInfo(NamedTuple):
12+
hits: int
13+
misses: int
14+
maxsize: Optional[int]
15+
currsize: int
16+
17+
class CacheParameters(TypedDict):
18+
maxsize: Optional[int]
19+
typed: bool
20+
21+
class LRUAsyncCallable(Protocol[AC]):
22+
__call__: AC
23+
@overload
24+
def __get__(
25+
self: LRUAsyncCallable[AC],
26+
instance: None,
27+
owner: Optional[type] = ...,
28+
) -> LRUAsyncCallable[AC]: ...
29+
@overload
30+
def __get__(
31+
self: LRUAsyncCallable[Callable[..., Awaitable[R]]],
32+
instance: object,
33+
owner: Optional[type] = ...,
34+
) -> LRUAsyncBoundCallable[Callable[..., Awaitable[R]]]: ...
35+
@property
36+
def __wrapped__(self) -> AC: ...
37+
def cache_parameters(self) -> CacheParameters: ...
38+
def cache_info(self) -> CacheInfo: ...
39+
def cache_clear(self) -> None: ...
40+
def cache_discard(self, *args: Any, **kwargs: Any) -> None: ...
41+
42+
class LRUAsyncBoundCallable(LRUAsyncCallable[AC]):
43+
__self__: object
44+
__call__: AC
45+
def __get__(
46+
self: LRUAsyncBoundCallable[AC],
47+
instance: Any,
48+
owner: Optional[type] = ...,
49+
) -> LRUAsyncBoundCallable[AC]: ...
50+
def __init__(self, lru: LRUAsyncCallable[AC], __self__: object) -> None: ...
51+
@property
52+
def __wrapped__(self) -> AC: ...
53+
@property
54+
def __func__(self) -> LRUAsyncCallable[AC]: ...
55+
56+
@overload
57+
def lru_cache(maxsize: AC, typed: bool = ...) -> LRUAsyncCallable[AC]: ...
58+
@overload
59+
def lru_cache(
60+
maxsize: Optional[int] = ..., typed: bool = ...
61+
) -> Callable[[AC], LRUAsyncCallable[AC]]: ...

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Source = "https://github.com/maxfischer2781/asyncstdlib"
4747
include = ["unittests"]
4848

4949
[tool.mypy]
50-
files = ["asyncstdlib/*.py"]
50+
files = ["asyncstdlib", "typetests"]
5151
check_untyped_defs = true
5252
no_implicit_optional = true
5353
warn_redundant_casts = true
@@ -62,3 +62,8 @@ disallow_untyped_decorators = true
6262
warn_return_any = true
6363
no_implicit_reexport = true
6464
strict_equality = true
65+
66+
[tool.pytest.ini_options]
67+
testpaths = [
68+
"unittests",
69+
]

typetests/README.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
=================
2+
MyPy Type Testing
3+
=================
4+
5+
This suite contains *type* tests for ``asyncstdlib``.
6+
These tests follow similar conventions to unittests but are checked by MyPy.
7+
8+
Test Files
9+
==========
10+
11+
Tests MUST be organised into files, with similar tests grouped together.
12+
Each test file SHOULD be called as per the pattern ``type_<scope>.py``,
13+
where ``<scope>`` describes what the tests cover;
14+
for example, ``test_functools.py`` type-tests the ``functools`` package.
15+
16+
An individual test is a function, method or class and SHOULD be named
17+
with a `test_` or `Test` prefix for functions/methods or classes, respectively.
18+
A class SHOULD be considered a test if it contains any tests.
19+
Tests MUST contain statements to be type-checked:
20+
- plain statements required to be type consistent,
21+
such as passing parameters of expected correct type to a function.
22+
- assertions about types and exhaustiveness,
23+
using `typing.assert_type` or `typing.assert_never`.
24+
- statements required to be type inconsistent with an expected type error,
25+
such as passing parameters of wrong type with `# type: ignore[arg-type]`.
26+
27+
Test files MAY contain non-test functions, methods or classes for use inside tests.
28+
These SHOULD be type-consistent and not require any type assertions or expected errors.
29+
30+
Test Execution
31+
==============
32+
33+
Tests MUST be checked by MyPy using
34+
the ``warn_unused_ignores`` configuration or ``--warn-unused-ignores`` command line
35+
option.
36+
This is required for negative type consistency checks,
37+
i.e. using expected type errors such as ``# type: ignore[arg-type]``.

typetests/test_functools.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from asyncstdlib import lru_cache
2+
3+
4+
@lru_cache()
5+
async def lru_function(a: int) -> int:
6+
return a
7+
8+
9+
async def test_cache_parameters() -> None:
10+
await lru_function(12)
11+
await lru_function("wrong parameter type") # type: ignore[arg-type]
12+
13+
14+
class TestLRUMethod:
15+
"""
16+
Test that `lru_cache` works on methods
17+
"""
18+
@lru_cache()
19+
async def cached(self) -> int:
20+
return 1
21+
22+
async def test_implicit_self(self) -> int:
23+
return await self.cached()

unittests/test_functools.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,32 @@ async def pingpong(arg):
203203
assert pingpong.cache_info().hits == (val + 1) * 2
204204

205205

206+
@sync
207+
async def test_lru_cache_method():
208+
"""
209+
Test that the lru_cache can be used on methods
210+
"""
211+
212+
class SelfCached:
213+
def __init__(self, ident: int):
214+
self.ident = ident
215+
216+
@a.lru_cache()
217+
async def pingpong(self, arg):
218+
# return identifier of instance to separate cache entries per instance
219+
return arg, self.ident
220+
221+
for iteration in range(4):
222+
instance = SelfCached(iteration)
223+
for val in range(20):
224+
# 1 read initializes, 2 reads hit
225+
assert await instance.pingpong(val) == (val, iteration)
226+
assert await instance.pingpong(float(val)) == (val, iteration)
227+
assert await instance.pingpong(val) == (val, iteration)
228+
assert instance.pingpong.cache_info().misses == val + 1 + 20 * iteration
229+
assert instance.pingpong.cache_info().hits == (val + 1 + 20 * iteration) * 2
230+
231+
206232
@sync
207233
async def test_lru_cache_bare():
208234
@a.lru_cache

0 commit comments

Comments
 (0)