diff --git a/asynq/decorators.pyi b/asynq/decorators.pyi index 012365c..d86ffe8 100644 --- a/asynq/decorators.pyi +++ b/asynq/decorators.pyi @@ -1,12 +1,12 @@ from typing import ( Any, + Awaitable, Callable, Coroutine, Generator, Generic, Mapping, Optional, - Type, TypeVar, Union, overload, @@ -17,13 +17,13 @@ from typing_extensions import ParamSpec from . import async_task, futures -_T = TypeVar("_T") # Return Type +_P = ParamSpec("_P") +_T = TypeVar("_T") _G = Generator[Any, Any, _T] # Generator that returns _T -OriginalFunctionParams = ParamSpec("OriginalFunctionParams") _Coroutine = Coroutine[Any, Any, _T] _CoroutineFn = Callable[..., _Coroutine] -def lazy(fn: Callable[..., _T]) -> Callable[..., futures.FutureBase[_T]]: ... +def lazy(fn: Callable[_P, _T]) -> Callable[_P, futures.FutureBase[_T]]: ... def has_async_fn(fn: object) -> bool: ... def is_pure_async_fn(fn: object) -> bool: ... def is_async_fn(fn: object) -> bool: ... @@ -35,18 +35,16 @@ def get_async_or_sync_fn(fn: object) -> Any: ... class PureAsyncDecoratorBinder(qcore.decorators.DecoratorBinder): def is_pure_async_fn(self) -> bool: ... -class PureAsyncDecorator( - qcore.decorators.DecoratorBase, Generic[_T, OriginalFunctionParams] -): +class PureAsyncDecorator(qcore.decorators.DecoratorBase, Generic[_T, _P]): binder_cls = PureAsyncDecoratorBinder @overload def __init__( self, - fn: Callable[OriginalFunctionParams, _T], - task_cls: Optional[Type[futures.FutureBase]], + fn: Callable[_P, Any], # TODO overloads for Generator[Any, Any, _T] and _T + task_cls: Optional[type[futures.FutureBase]], kwargs: Mapping[str, Any] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., ) -> None: ... @overload def __init__( @@ -54,33 +52,31 @@ class PureAsyncDecorator( fn: Callable[OriginalFunctionParams, Generator[Any, Any, _T]], task_cls: Optional[Type[futures.FutureBase]], kwargs: Mapping[str, Any] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., ) -> None: ... def name(self) -> str: ... def is_pure_async_fn(self) -> bool: ... def asyncio( - self, - *args: OriginalFunctionParams.args, - **kwargs: OriginalFunctionParams.kwargs, + self, *args: _P.args, **kwargs: _P.kwargs ) -> Coroutine[Any, Any, _T]: ... def __call__( self, *args: Any, **kwargs: Any ) -> Union[_T, futures.FutureBase[_T]]: ... - def __get__(self, owner: Any, cls: Any) -> PureAsyncDecorator[_T]: ... # type: ignore + def __get__(self, owner: Any, cls: Any) -> PureAsyncDecorator[_T, _P]: ... # type: ignore[override] class AsyncDecoratorBinder(qcore.decorators.DecoratorBinder, Generic[_T]): def asynq(self, *args: Any, **kwargs: Any) -> async_task.AsyncTask[_T]: ... def asyncio(self, *args, **kwargs) -> Coroutine[Any, Any, _T]: ... -class AsyncDecorator(PureAsyncDecorator[_T, OriginalFunctionParams]): +class AsyncDecorator(PureAsyncDecorator[_T, _P]): binder_cls = AsyncDecoratorBinder # type: ignore @overload def __init__( self, - fn: Callable[OriginalFunctionParams, _T], - cls: Optional[Type[futures.FutureBase]], + fn: Callable[_P, Any], # TODO overloads for Generator[Any, Any, _T] and _T + cls: Optional[type[futures.FutureBase]], kwargs: Mapping[str, Any] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., ): ... @overload def __init__( @@ -88,35 +84,35 @@ class AsyncDecorator(PureAsyncDecorator[_T, OriginalFunctionParams]): fn: Callable[OriginalFunctionParams, Generator[Any, Any, _T]], cls: Optional[Type[futures.FutureBase]], kwargs: Mapping[str, Any] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., ): ... def is_pure_async_fn(self) -> bool: ... def asynq(self, *args: Any, **kwargs: Any) -> async_task.AsyncTask[_T]: ... - def __call__(self, *args: Any, **kwargs: Any) -> _T: ... - def __get__(self, owner: Any, cls: Any) -> AsyncDecorator[_T]: ... # type: ignore + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T: ... + def __get__(self, owner: Any, cls: Any) -> AsyncDecorator[_T, _P]: ... # type: ignore[override] class AsyncAndSyncPairDecoratorBinder(AsyncDecoratorBinder[_T]): ... -class AsyncAndSyncPairDecorator(AsyncDecorator[_T, OriginalFunctionParams]): +class AsyncAndSyncPairDecorator(AsyncDecorator[_T, _P]): binder_cls = AsyncAndSyncPairDecoratorBinder # type: ignore def __init__( self, fn: Callable[..., futures.FutureBase[_T]], - cls: Optional[Type[futures.FutureBase]], + cls: Optional[type[futures.FutureBase]], sync_fn: Callable[..., _T], kwargs: Mapping[str, Any] = ..., ) -> None: ... def __call__(self, *args: Any, **kwargs: Any) -> _T: ... def __get__(self, owner: Any, cls: Any) -> Any: ... -class AsyncProxyDecorator(AsyncDecorator[_T, OriginalFunctionParams]): +class AsyncProxyDecorator(AsyncDecorator[_T, _P]): def __init__( self, fn: Callable[..., futures.FutureBase[_T]], asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., ) -> None: ... -class AsyncAndSyncPairProxyDecorator(AsyncProxyDecorator[_T, OriginalFunctionParams]): +class AsyncAndSyncPairProxyDecorator(AsyncProxyDecorator[_T, _P]): def __init__( self, fn: Callable[..., futures.FutureBase[_T]], @@ -128,50 +124,50 @@ class AsyncAndSyncPairProxyDecorator(AsyncProxyDecorator[_T, OriginalFunctionPar class _MkAsyncDecorator: @overload def __call__( - self, fn: Callable[OriginalFunctionParams, Generator[Any, Any, _T]] - ) -> AsyncDecorator[_T, OriginalFunctionParams]: ... + self, fn: Callable[_P, Generator[Any, Any, _T]] + ) -> AsyncDecorator[_T, _P]: ... @overload def __call__( - self, fn: Callable[OriginalFunctionParams, _T] - ) -> AsyncDecorator[_T, OriginalFunctionParams]: ... + self, fn: Callable[_P, _T] + ) -> AsyncDecorator[_T, _P]: ... class _MkPureAsyncDecorator: @overload def __call__( - self, fn: Callable[OriginalFunctionParams, Generator[Any, Any, _T]] - ) -> PureAsyncDecorator[_T, OriginalFunctionParams]: ... + self, fn: Callable[_P, Generator[Any, Any, _T]] + ) -> PureAsyncDecorator[_T, _P]: ... @overload def __call__( - self, fn: Callable[OriginalFunctionParams, _T] - ) -> PureAsyncDecorator[_T, OriginalFunctionParams]: ... + self, fn: Callable[_P, _T] + ) -> PureAsyncDecorator[_T, _P]: ... # In reality these two can return other Decorator subclasses, but that doesn't matter for callers. @overload def asynq( # type: ignore *, - sync_fn: Optional[Callable[OriginalFunctionParams, _T]] = ..., + sync_fn: Optional[Callable[_P, _T]] = ..., cls: Type[futures.FutureBase] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., **kwargs: Any, ) -> _MkAsyncDecorator: ... @overload def asynq( # type: ignore *, - sync_fn: Optional[Callable[OriginalFunctionParams, Generator[Any, Any, _T]]] = ..., + sync_fn: Optional[Callable[_P, Generator[Any, Any, _T]]] = ..., cls: Type[futures.FutureBase] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., **kwargs: Any, ) -> _MkAsyncDecorator: ... @overload def asynq( pure: bool, sync_fn: Optional[ - Callable[OriginalFunctionParams, Union[_T, Generator[Any, Any, _T]]] + Callable[_P, Union[_T, Generator[Any, Any, _T]]] ] = ..., cls: Type[futures.FutureBase] = ..., - asyncio_fn: Optional[Callable[..., Coroutine[Any, Any, _T]]] = ..., + asyncio_fn: Optional[Callable[_P, Coroutine[Any, Any, _T]]] = ..., **kwargs: Any, -) -> _MkPureAsyncDecorator: ... # type: ignore +) -> _MkPureAsyncDecorator: ... @overload def async_proxy( *, diff --git a/asynq/tests/test_typing.py b/asynq/tests/test_typing.py new file mode 100644 index 0000000..33fa579 --- /dev/null +++ b/asynq/tests/test_typing.py @@ -0,0 +1,41 @@ +from typing import Any, Generator, TYPE_CHECKING +from typing_extensions import assert_type +from asynq.decorators import async_call, lazy, asynq +from asynq.futures import FutureBase + + +def test_lazy() -> None: + @lazy + def lazy_func(x: int) -> str: + return str(x) + + if TYPE_CHECKING: + assert_type(lazy_func(1), FutureBase[str]) + + +def test_dot_asyncio() -> None: + @asynq() + def non_generator(x: int) -> str: + return str(x) + + @asynq() + def generator(x: int) -> Generator[Any, Any, str]: + yield None + return str(x) + + async def caller() -> None: + # This doesn't work, apparently due to a mypy bug + assert_type(await generator.asyncio(1), str) # type: ignore[assert-type] + assert_type(await non_generator.asyncio(1), str) # type: ignore[assert-type] + + await non_generator.asyncio() # type: ignore[call-arg] + await generator.asyncio() # type: ignore[call-arg] + + +def test_async_call() -> None: + def f(x: int) -> str: + return str(x) + + async_call(f, 1) + if TYPE_CHECKING: + async_call(f, 1, task_cls=FutureBase) # TODO: this should be an error diff --git a/pyproject.toml b/pyproject.toml index e4c3332..f46c3dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,3 +13,6 @@ exclude = ''' | \.eggs )/ ''' + +[tool.mypy] +warn_unused_ignores = true diff --git a/requirements.txt b/requirements.txt index 9874d7f..6443045 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ Cython>=0.27.1 qcore pygments black==24.3.0 -mypy==1.4.1 +mypy==1.10.1 typing_extensions==4.11.0 \ No newline at end of file