Repository: quora/pyanalyze
Pull request #300 (2021-12-03 02:06:20)

UnboundMethodValue improvements


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index bca28a5..e700ded 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Enhance `reveal_type()` output for `UnboundMethodValue` (#300)
 - Fix handling of `async for` (#298)
 - Add support for stub-only Protocols (#295)
 - Basic support for stub-only types (#290)
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index 3b2d5f0..2c82fc1 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -34,6 +34,7 @@ from .value import (
     TypeVarValue,
     NO_RETURN_VALUE,
     KNOWN_MUTABLE_TYPES,
+    UnboundMethodValue,
     Value,
     WeakExtension,
     make_weak,
@@ -827,8 +828,12 @@ def _reveal_type_impl(ctx: CallContext) -> Value:
         message = f"Revealed type is '{value!s}'"
         if isinstance(value, KnownValue):
             sig = ctx.visitor.arg_spec_cache.get_argspec(value.val)
-            if sig is not None:
-                message += f", signature is {sig!s}"
+        elif isinstance(value, UnboundMethodValue):
+            sig = value.get_signature(ctx.visitor)
+        else:
+            sig = None
+        if sig is not None:
+            message += f", signature is {sig!s}"
         ctx.show_error(message, ErrorCode.inference_failure, arg="value")
     return KnownValue(None)
 
diff --git a/pyanalyze/importer.py b/pyanalyze/importer.py
index 6af58c0..05aa1ae 100644
--- a/pyanalyze/importer.py
+++ b/pyanalyze/importer.py
@@ -5,6 +5,7 @@ Module responsible for importing files.
 """
 
 from functools import lru_cache
+import importlib
 import importlib.util
 from pathlib import Path
 import sys
@@ -99,7 +100,7 @@ def load_module_from_file(
     return import_module(str(abspath), abspath), False
 
 
-def import_module(module_path: str, filename: str) -> ModuleType:
+def import_module(module_path: str, filename: Path) -> ModuleType:
     """Import a file under an arbitrary module name."""
     spec = importlib.util.spec_from_file_location(module_path, filename)
     module = importlib.util.module_from_spec(spec)
diff --git a/pyanalyze/node_visitor.py b/pyanalyze/node_visitor.py
index 38958be..a4aec9a 100644
--- a/pyanalyze/node_visitor.py
+++ b/pyanalyze/node_visitor.py
@@ -149,7 +149,7 @@ class BaseNodeVisitor(ast.NodeVisitor):
 
     _changes_for_fixer = collections.defaultdict(list)
 
-    tree: Optional[ast.Module]
+    tree: ast.Module
     all_failures: List[Failure]
 
     def __init__(
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index 0e5fb09..fda010d 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -33,13 +33,14 @@ from collections.abc import (
     Sized,
 )
 from contextlib import AbstractContextManager
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 import collections.abc
 import qcore
 import inspect
 import sys
 from types import GeneratorType
 from typing import (
+    Dict,
     Set,
     Tuple,
     cast,
@@ -93,12 +94,16 @@ _TYPING_ALIASES = {
 }
 
 
-class TypeshedFinder(object):
-    def __init__(self, verbose: bool = True) -> None:
-        self.verbose = verbose
-        self.resolver = typeshed_client.Resolver()
-        self._assignment_cache = {}
-        self._attribute_cache = {}
+@dataclass
+class TypeshedFinder:
+    verbose: bool = True
+    resolver: typeshed_client.Resolver = field(default_factory=typeshed_client.Resolver)
+    _assignment_cache: Dict[Tuple[str, ast3.AST], Value] = field(
+        default_factory=dict, repr=False, init=False
+    )
+    _attribute_cache: Dict[Tuple[str, str, bool], Value] = field(
+        default_factory=dict, repr=False, init=False
+    )
 
     def log(self, message: str, obj: object) -> None:
         if not self.verbose:
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 51edd71..626b4d6 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -468,6 +468,24 @@ class UnboundMethodValue(Value):
     def get_type_value(self) -> Value:
         return KnownValue(type(self.get_method()))
 
+    def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
+        signature = self.get_signature(ctx)
+        if signature is None:
+            return {}
+        return CallableValue(signature).can_assign(other, ctx)
+
+    def get_signature(
+        self, ctx: CanAssignContext
+    ) -> Optional["pyanalyze.signature.Signature"]:
+        signature = ctx.signature_from_value(self)
+        if signature is None:
+            return None
+        if isinstance(signature, pyanalyze.signature.BoundMethodSignature):
+            signature = signature.get_signature()
+        if isinstance(signature, pyanalyze.signature.PropertyArgSpec):
+            return None
+        return signature
+
     def substitute_typevars(self, typevars: TypeVarMap) -> "Value":
         return UnboundMethodValue(
             self.attr_name,


Test patch:
diff --git a/pyanalyze/test_value.py b/pyanalyze/test_value.py
index ff361f9..01906f4 100644
--- a/pyanalyze/test_value.py
+++ b/pyanalyze/test_value.py
@@ -1,3 +1,4 @@
+import ast
 import collections.abc
 import enum
 import io
@@ -12,6 +13,8 @@ from unittest import mock
 from . import tests
 from . import value
 from .checker import Checker
+from .name_check_visitor import NameCheckVisitor
+from .signature import Signature, MaybeSignature
 from .stacked_scopes import Composite
 from .test_config import TestConfig
 from .type_object import TypeObject
@@ -19,6 +22,7 @@ from .value import (
     AnnotatedValue,
     AnySource,
     AnyValue,
+    CallableValue,
     CanAssignError,
     GenericBases,
     Value,
@@ -37,6 +41,7 @@ from .value import (
 class Context(CanAssignContext):
     def __init__(self) -> None:
         self.checker = Checker(TestConfig())
+        self.visitor = NameCheckVisitor("", "", ast.parse(""), checker=self.checker)
 
     def make_type_object(self, typ: Union[type, super]) -> TypeObject:
         return self.checker.make_type_object(typ)
@@ -46,6 +51,9 @@ class Context(CanAssignContext):
     ) -> GenericBases:
         return self.checker.arg_spec_cache.get_generic_bases(typ, generic_args)
 
+    def signature_from_value(self, value: Value) -> MaybeSignature:
+        return self.visitor.signature_from_value(value)
+
 
 CTX = Context()
 
@@ -91,9 +99,8 @@ def test_known_value() -> None:
 
 
 def test_unbound_method_value() -> None:
-    val = value.UnboundMethodValue(
-        "get_prop_with_get", Composite(value.TypedValue(tests.PropertyObject))
-    )
+    po_composite = Composite(value.TypedValue(tests.PropertyObject))
+    val = value.UnboundMethodValue("get_prop_with_get", po_composite)
     assert "<method get_prop_with_get on pyanalyze.tests.PropertyObject>" == str(val)
     assert "get_prop_with_get" == val.attr_name
     assert TypedValue(tests.PropertyObject) == val.composite.value
@@ -103,9 +110,7 @@ def test_unbound_method_value() -> None:
     assert not val.is_type(str)
 
     val = value.UnboundMethodValue(
-        "get_prop_with_get",
-        Composite(value.TypedValue(tests.PropertyObject)),
-        secondary_attr_name="asynq",
+        "get_prop_with_get", po_composite, secondary_attr_name="asynq"
     )
     assert "<method get_prop_with_get.asynq on pyanalyze.tests.PropertyObject>" == str(
         val
@@ -114,12 +119,20 @@ def test_unbound_method_value() -> None:
     assert TypedValue(tests.PropertyObject) == val.composite.value
     assert "asynq" == val.secondary_attr_name
     method = val.get_method()
-    assert None is not method
+    assert method is not None
     assert method.__name__ in tests.ASYNQ_METHOD_NAMES
     assert tests.PropertyObject.get_prop_with_get == method.__self__
     assert val.is_type(object)
     assert not val.is_type(str)
 
+    val = value.UnboundMethodValue("non_async_method", po_composite)
+    assert val.get_method() is not None
+    assert val.get_signature(CTX) is not None
+    assert_can_assign(val, val)
+    assert_cannot_assign(val, KnownValue(1))
+    assert_can_assign(val, CallableValue(Signature.make([], is_ellipsis_args=True)))
+    assert_can_assign(val, CallableValue(Signature.make([])))
+
 
 def test_typed_value() -> None:
     val = TypedValue(str)