Skip to content

Commit

Permalink
Type hinting for lru_cache on methods (#105)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maxfischer2781 authored Apr 30, 2023
1 parent b966a95 commit a6fc497
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignore:
# type tests are not execute, so there is no code coverage
- "typetests"
61 changes: 61 additions & 0 deletions asyncstdlib/_lrucache.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from ._typing import AC, Protocol, R as R, TypedDict
from typing import (
Any,
Awaitable,
Callable,
NamedTuple,
Optional,
overload,
)

class CacheInfo(NamedTuple):
hits: int
misses: int
maxsize: Optional[int]
currsize: int

class CacheParameters(TypedDict):
maxsize: Optional[int]
typed: bool

class LRUAsyncCallable(Protocol[AC]):
__call__: AC
@overload
def __get__(
self: LRUAsyncCallable[AC],
instance: None,
owner: Optional[type] = ...,
) -> LRUAsyncCallable[AC]: ...
@overload
def __get__(
self: LRUAsyncCallable[Callable[..., Awaitable[R]]],
instance: object,
owner: Optional[type] = ...,
) -> LRUAsyncBoundCallable[Callable[..., Awaitable[R]]]: ...
@property
def __wrapped__(self) -> AC: ...
def cache_parameters(self) -> CacheParameters: ...
def cache_info(self) -> CacheInfo: ...
def cache_clear(self) -> None: ...
def cache_discard(self, *args: Any, **kwargs: Any) -> None: ...

class LRUAsyncBoundCallable(LRUAsyncCallable[AC]):
__self__: object
__call__: AC
def __get__(
self: LRUAsyncBoundCallable[AC],
instance: Any,
owner: Optional[type] = ...,
) -> LRUAsyncBoundCallable[AC]: ...
def __init__(self, lru: LRUAsyncCallable[AC], __self__: object) -> None: ...
@property
def __wrapped__(self) -> AC: ...
@property
def __func__(self) -> LRUAsyncCallable[AC]: ...

@overload
def lru_cache(maxsize: AC, typed: bool = ...) -> LRUAsyncCallable[AC]: ...
@overload
def lru_cache(
maxsize: Optional[int] = ..., typed: bool = ...
) -> Callable[[AC], LRUAsyncCallable[AC]]: ...
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Source = "https://github.com/maxfischer2781/asyncstdlib"
include = ["unittests"]

[tool.mypy]
files = ["asyncstdlib/*.py"]
files = ["asyncstdlib", "typetests"]
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
Expand All @@ -62,3 +62,8 @@ disallow_untyped_decorators = true
warn_return_any = true
no_implicit_reexport = true
strict_equality = true

[tool.pytest.ini_options]
testpaths = [
"unittests",
]
37 changes: 37 additions & 0 deletions typetests/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
=================
MyPy Type Testing
=================

This suite contains *type* tests for ``asyncstdlib``.
These tests follow similar conventions to unittests but are checked by MyPy.

Test Files
==========

Tests MUST be organised into files, with similar tests grouped together.
Each test file SHOULD be called as per the pattern ``type_<scope>.py``,
where ``<scope>`` describes what the tests cover;
for example, ``test_functools.py`` type-tests the ``functools`` package.

An individual test is a function, method or class and SHOULD be named
with a `test_` or `Test` prefix for functions/methods or classes, respectively.
A class SHOULD be considered a test if it contains any tests.
Tests MUST contain statements to be type-checked:
- plain statements required to be type consistent,
such as passing parameters of expected correct type to a function.
- assertions about types and exhaustiveness,
using `typing.assert_type` or `typing.assert_never`.
- statements required to be type inconsistent with an expected type error,
such as passing parameters of wrong type with `# type: ignore[arg-type]`.

Test files MAY contain non-test functions, methods or classes for use inside tests.
These SHOULD be type-consistent and not require any type assertions or expected errors.

Test Execution
==============

Tests MUST be checked by MyPy using
the ``warn_unused_ignores`` configuration or ``--warn-unused-ignores`` command line
option.
This is required for negative type consistency checks,
i.e. using expected type errors such as ``# type: ignore[arg-type]``.
23 changes: 23 additions & 0 deletions typetests/test_functools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from asyncstdlib import lru_cache


@lru_cache()
async def lru_function(a: int) -> int:
return a


async def test_cache_parameters() -> None:
await lru_function(12)
await lru_function("wrong parameter type") # type: ignore[arg-type]


class TestLRUMethod:
"""
Test that `lru_cache` works on methods
"""
@lru_cache()
async def cached(self) -> int:
return 1

async def test_implicit_self(self) -> int:
return await self.cached()
26 changes: 26 additions & 0 deletions unittests/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,32 @@ async def pingpong(arg):
assert pingpong.cache_info().hits == (val + 1) * 2


@sync
async def test_lru_cache_method():
"""
Test that the lru_cache can be used on methods
"""

class SelfCached:
def __init__(self, ident: int):
self.ident = ident

@a.lru_cache()
async def pingpong(self, arg):
# return identifier of instance to separate cache entries per instance
return arg, self.ident

for iteration in range(4):
instance = SelfCached(iteration)
for val in range(20):
# 1 read initializes, 2 reads hit
assert await instance.pingpong(val) == (val, iteration)
assert await instance.pingpong(float(val)) == (val, iteration)
assert await instance.pingpong(val) == (val, iteration)
assert instance.pingpong.cache_info().misses == val + 1 + 20 * iteration
assert instance.pingpong.cache_info().hits == (val + 1 + 20 * iteration) * 2


@sync
async def test_lru_cache_bare():
@a.lru_cache
Expand Down

0 comments on commit a6fc497

Please sign in to comment.