Repository: quora/pyanalyze
Pull request #314 (2021-12-05 00:46:42)

support bare AsynqCallable


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 72b0763..fb90755 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Support `AsynqCallable` with no arguments as an annotation (#314)
 - Support iteration over old-style iterables providing only `__getitem__` (#313)
 - Add support for runtime Protocols (#311)
 - Stop inferring `Any` for non-runtime checkable Protocols on Python 3.6 and 3.7 (#310)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index 6ec7868..6555b22 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -329,6 +329,8 @@ def _type_from_runtime(val: Any, ctx: Context, is_typeddict: bool = False) -> Va
     elif typing_inspect.is_callable_type(val):
         args = typing_inspect.get_args(val)
         return _value_of_origin_args(Callable, args, val, ctx)
+    elif val is AsynqCallable:
+        return CallableValue(Signature.make([], is_ellipsis_args=True, is_asynq=True))
     elif isinstance(val, type):
         return _maybe_typed_value(val)
     elif val is None:
diff --git a/pyanalyze/extensions.py b/pyanalyze/extensions.py
index 065f71e..61c0d95 100644
--- a/pyanalyze/extensions.py
+++ b/pyanalyze/extensions.py
@@ -114,7 +114,7 @@ class NoAny(CustomCheck):
 class _AsynqCallableMeta(type):
     def __getitem__(
         self, params: Tuple[Union[Literal[Ellipsis], List[object]], object]
-    ) -> "AsynqCallable":
+    ) -> Any:
         if not isinstance(params, tuple) or len(params) != 2:
             raise TypeError(
                 "AsynqCallable[...] should be instantiated "
@@ -145,7 +145,8 @@ class AsynqCallable(metaclass=_AsynqCallableMeta):
     args: Union[Literal[Ellipsis], Tuple[object, ...]]
     return_type: object
 
-    def __getitem__(self, item: object) -> "AsynqCallable":
+    # Returns AsynqCallable but pyanalyze interprets that as AsynqCallable[..., Any]
+    def __getitem__(self, item: object) -> Any:
         if not isinstance(item, tuple):
             item = (item,)
         params = self.__parameters__


Test patch:
diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py
index 51b338a..966a0e5 100644
--- a/pyanalyze/test_annotations.py
+++ b/pyanalyze/test_annotations.py
@@ -7,6 +7,7 @@ from .value import (
     AnnotatedValue,
     AnySource,
     AnyValue,
+    CallableValue,
     KnownValue,
     MultiValuedValue,
     NewTypeValue,
@@ -741,7 +742,7 @@ class TestCallable(TestNameCheckVisitorBase):
         def capybara() -> None:
             takes_callable(wrong_callable)
 
-    @assert_fails(ErrorCode.incompatible_argument)
+    @assert_passes()
     def test_known_value_error(self):
         from typing_extensions import Literal
 
@@ -755,9 +756,9 @@ class TestCallable(TestNameCheckVisitorBase):
             return True
 
         def capybara() -> None:
-            g(h)
+            g(h)  # E: incompatible_argument
 
-    @assert_fails(ErrorCode.incompatible_argument)
+    @assert_passes()
     def test_asynq_callable_incompatible(self):
         from typing import Callable
         from pyanalyze.extensions import AsynqCallable
@@ -766,9 +767,9 @@ class TestCallable(TestNameCheckVisitorBase):
             pass
 
         def capybara(func: Callable[[], int]) -> None:
-            f(func)
+            f(func)  # E: incompatible_argument
 
-    @assert_fails(ErrorCode.incompatible_argument)
+    @assert_passes()
     def test_asynq_callable_incompatible_literal(self):
         from pyanalyze.extensions import AsynqCallable
 
@@ -779,18 +780,27 @@ class TestCallable(TestNameCheckVisitorBase):
             return 0
 
         def capybara() -> None:
-            f(func)
+            f(func)  # E: incompatible_argument
 
     @assert_passes()
     def test_asynq_callable(self):
         from asynq import asynq
         from pyanalyze.extensions import AsynqCallable
+        from pyanalyze.signature import Signature
         from typing import Optional
 
         @asynq()
         def func_example(x: int) -> str:
             return ""
 
+        sig = Signature.make([], is_asynq=True, is_ellipsis_args=True)
+
+        @asynq()
+        def bare_asynq_callable(fn: AsynqCallable) -> None:
+            assert_is_value(fn, CallableValue(sig))
+            yield fn.asynq()
+            yield fn.asynq("some", "arguments")
+
         @asynq()
         def caller(
             func: AsynqCallable[[int], str],
@@ -802,6 +812,7 @@ class TestCallable(TestNameCheckVisitorBase):
             yield caller.asynq(func_example)
             if func2 is not None:
                 yield func2.asynq(1)
+            yield bare_asynq_callable.asynq(func_example)
 
     @assert_passes(settings={ErrorCode.impure_async_call: False})
     def test_amap(self):