diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..d19a3b7 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,3 @@ +ignore: + # type tests are not execute, so there is no code coverage + - "typetests" diff --git a/asyncstdlib/_lrucache.pyi b/asyncstdlib/_lrucache.pyi new file mode 100644 index 0000000..38794cc --- /dev/null +++ b/asyncstdlib/_lrucache.pyi @@ -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]]: ... diff --git a/pyproject.toml b/pyproject.toml index 1b46000..81223ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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", +] diff --git a/typetests/README.rst b/typetests/README.rst new file mode 100644 index 0000000..361fa3e --- /dev/null +++ b/typetests/README.rst @@ -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_.py``, +where ```` 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]``. diff --git a/typetests/test_functools.py b/typetests/test_functools.py new file mode 100644 index 0000000..361971e --- /dev/null +++ b/typetests/test_functools.py @@ -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() diff --git a/unittests/test_functools.py b/unittests/test_functools.py index 9d13530..6e6e7dc 100644 --- a/unittests/test_functools.py +++ b/unittests/test_functools.py @@ -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