Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type hinting for lru_cache on methods #105

Merged
merged 13 commits into from
Apr 30, 2023
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 @@ -44,7 +44,7 @@ Documentation = "https://asyncstdlib.readthedocs.io/en/latest/"
Source = "https://github.com/maxfischer2781/asyncstdlib"

[tool.mypy]
files = ["asyncstdlib/*.py"]
files = ["asyncstdlib", "typetests"]
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
Expand All @@ -59,3 +59,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