Repository: quora/pyanalyze
Pull request #369 (2021-12-26 12:14:08)

SubclassValue signatures


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index af7751a..a0de561 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Infer call signatures for `Type[X]` (#369)
 - Support configuration in a `pyproject.toml` file (#368)
 - Require `typeshed_client` 2.0 (#361)
 - Add JSON output for integrating pyanalyze's output with other
diff --git a/pyanalyze/format_strings.py b/pyanalyze/format_strings.py
index 4207a5c..71c9142 100644
--- a/pyanalyze/format_strings.py
+++ b/pyanalyze/format_strings.py
@@ -223,7 +223,7 @@ class PercentFormatString:
     pattern: Union[bytes, str]
     is_bytes: bool = False
     specifiers: Sequence[ConversionSpecifier] = ()
-    raw_pieces: Sequence[str] = ()
+    raw_pieces: Union[Sequence[bytes], Sequence[str]] = ()
 
     @classmethod
     def from_pattern(cls, pattern: str) -> "PercentFormatString":
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 87718c1..72f539e 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -4537,10 +4537,21 @@ class NameCheckVisitor(
                 return None
             return bound_method.get_signature()
         elif isinstance(value, SubclassValue):
-            # SubclassValues are callable, but we can't assume the signature
-            # is consistent with the base class.
-            # TODO: make the return annotation be of the type of the value.
-            return ANY_SIGNATURE
+            if isinstance(value.typ, TypedValue):
+                if isinstance(value.typ.typ, type):
+                    if value.typ.typ is tuple:
+                        # Probably an unknown namedtuple
+                        return ANY_SIGNATURE
+                    argspec = self.arg_spec_cache.get_argspec(value.typ.typ)
+                    if argspec is None:
+                        return ANY_SIGNATURE
+                    return argspec
+                else:
+                    # TODO synthetic types
+                    return ANY_SIGNATURE
+            else:
+                # TODO generic SubclassValue
+                return ANY_SIGNATURE
         elif isinstance(value, AnyValue):
             return ANY_SIGNATURE
         else:
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index f5c94bd..7295b6b 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -324,7 +324,7 @@ class Signature:
     """What the callable returns."""
     impl: Optional[Impl] = field(default=None, compare=False)
     """:term:`impl` function for this signature."""
-    callable: Optional[Callable[..., Any]] = field(default=None, compare=False)
+    callable: Optional[object] = field(default=None, compare=False)
     """The callable that this signature represents."""
     is_asynq: bool = False
     """Whether this signature represents an asynq function."""
@@ -827,7 +827,7 @@ class Signature:
     def _maybe_perform_call(
         self, actual_args: ActualArguments, visitor: "NameCheckVisitor", node: ast.AST
     ) -> Optional[Value]:
-        if self.callable is None:
+        if self.callable is None or not callable(self.callable):
             return None
         args = []
         kwargs = {}


Test patch:
diff --git a/pyanalyze/test_arg_spec.py b/pyanalyze/test_arg_spec.py
index 21a6d05..8deef74 100644
--- a/pyanalyze/test_arg_spec.py
+++ b/pyanalyze/test_arg_spec.py
@@ -395,6 +395,18 @@ class TestClassInstantiation(TestNameCheckVisitorBase):
         def capybara():
             FailingImpl()  # E: incompatible_call
 
+    @assert_passes()
+    def test_subclass_value(self):
+        from typing import Type
+
+        class A:
+            def __init__(self, x: int) -> None:
+                pass
+
+        def capybara(t: Type[A]) -> None:
+            assert_is_value(t(1), TypedValue(A))
+            t("x")  # E: incompatible_argument
+
 
 class TestFunctionsSafeToCall(TestNameCheckVisitorBase):
     @assert_passes()