Repository: quora/pyanalyze
Pull request #513 (2022-04-08 20:47:52)

Fix handling of overloaded methods called on literals


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index b78a4b5..f0c6392 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Fix handling of overloaded methods called on literals (#513)
 - Partial support for running on Python 3.11 (#512)
 - Basic support for checking `Final` and for checking re-assignments
   to variables declared with a specific type (#505)
diff --git a/pyanalyze/arg_spec.py b/pyanalyze/arg_spec.py
index 968c3af..80e02b4 100644
--- a/pyanalyze/arg_spec.py
+++ b/pyanalyze/arg_spec.py
@@ -561,23 +561,6 @@ class ArgSpecCache:
         is_asynq: bool,
         in_overload_resolution: bool,
     ) -> MaybeSignature:
-        if not in_overload_resolution:
-            fq_name = get_fully_qualified_name(obj)
-            if fq_name is not None:
-                overloads = get_overloads(fq_name)
-                if overloads:
-                    sigs = [
-                        self._cached_get_argspec(
-                            overload, impl, is_asynq, in_overload_resolution=True
-                        )
-                        for overload in overloads
-                    ]
-                    if all_of_type(sigs, Signature):
-                        return OverloadedSignature(sigs)
-                evaluator_sig = self._maybe_make_evaluator_sig(obj, impl, is_asynq)
-                if evaluator_sig is not None:
-                    return evaluator_sig
-
         if isinstance(obj, tuple):
             return None  # lost cause
 
@@ -610,6 +593,25 @@ class ArgSpecCache:
             )
             return make_bound_method(argspec, Composite(KnownValue(obj.__self__)))
 
+        # Must be after the check for bound methods, because otherwise we
+        # won't bind self correctly.
+        if not in_overload_resolution:
+            fq_name = get_fully_qualified_name(obj)
+            if fq_name is not None:
+                overloads = get_overloads(fq_name)
+                if overloads:
+                    sigs = [
+                        self._cached_get_argspec(
+                            overload, impl, is_asynq, in_overload_resolution=True
+                        )
+                        for overload in overloads
+                    ]
+                    if all_of_type(sigs, Signature):
+                        return OverloadedSignature(sigs)
+                evaluator_sig = self._maybe_make_evaluator_sig(obj, impl, is_asynq)
+                if evaluator_sig is not None:
+                    return evaluator_sig
+
         if hasattr_static(obj, "fn") or hasattr_static(obj, "original_fn"):
             is_asynq = is_asynq or hasattr_static(obj, "asynq")
             # many decorators put the original function in the .fn attribute


Test patch:
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index 0260e63..d10dabd 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -1119,6 +1119,31 @@ class TestOverload(TestNameCheckVisitorBase):
             assert_type(func(1, 1), int)
             assert_type(func("x"), float)
 
+    @assert_passes()
+    def test_overloaded_method(self):
+        from pyanalyze.extensions import overload, assert_type
+
+        class Capybara:
+            @overload
+            def meth(self, x: int) -> str:
+                ...
+
+            @overload
+            def meth(self, x: str) -> int:
+                ...
+
+            def meth(self, x: object) -> object:
+                raise NotImplementedError
+
+        cap = Capybara()
+
+        def caller(inst: Capybara) -> None:
+            assert_type(cap.meth(1), str)
+            assert_type(cap.meth(""), int)
+
+            assert_type(inst.meth(1), str)
+            assert_type(inst.meth(""), int)
+
 
 class TestSelfAnnotation(TestNameCheckVisitorBase):
     @assert_passes()