Repository: quora/pyanalyze
Pull request #304 (2021-12-03 04:43:59)

Better detect signatures of methods in stub files


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index d4aad1f..2a175f9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Better detect signatures of methods in stub files (#304)
 - Improve handling of positional-only arguments in stub files (#303)
 - Fix bug where pyanalyze incorrectly inferred that an attribute always exists (#302)
 - Fix compatibility of signatures with extra parameters (#301)
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index d75ad7d..9f33b1a 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -127,6 +127,24 @@ class TypeshedFinder:
         if inspect.ismethod(obj):
             self.log("Ignoring method", obj)
             return None
+        if (
+            hasattr(obj, "__qualname__")
+            and hasattr(obj, "__name__")
+            and hasattr(obj, "__module__")
+            and obj.__qualname__ != obj.__name__
+            and "." in obj.__qualname__
+        ):
+            parent_name, own_name = obj.__qualname__.rsplit(".", maxsplit=1)
+            parent_fqn = f"{obj.__module__}.{parent_name}"
+            parent_info = self._get_info_for_name(parent_fqn)
+            if parent_info is not None:
+                maybe_info = self._get_child_info(parent_info, own_name, obj.__module__)
+                if maybe_info is not None:
+                    info, mod = maybe_info
+                    fq_name = f"{parent_fqn}.{own_name}"
+                    sig = self._get_signature_from_info(info, obj, fq_name, mod)
+                    return sig
+
         fq_name = self._get_fq_name(obj)
         if fq_name is None:
             return None
@@ -341,6 +359,24 @@ class TypeshedFinder:
                 return UNINITIALIZED_VALUE
         return UNINITIALIZED_VALUE
 
+    def _get_child_info(
+        self, info: typeshed_client.resolver.ResolvedName, attr: str, mod: str
+    ) -> Optional[Tuple[typeshed_client.resolver.ResolvedName, str]]:
+        if info is None:
+            return None
+        elif isinstance(info, typeshed_client.ImportedInfo):
+            return self._get_child_info(info.info, attr, ".".join(info.source_module))
+        elif isinstance(info, typeshed_client.NameInfo):
+            if isinstance(info.ast, ast3.ClassDef):
+                if info.child_nodes and attr in info.child_nodes:
+                    return info.child_nodes[attr], mod
+                return None
+            elif isinstance(info.ast, ast3.Assign):
+                return None  # TODO maybe we need this for aliased methods
+            else:
+                return None
+        return None
+
     def _has_own_attribute(self, typ: Union[type, str], attr: str) -> bool:
         # Special case since otherwise we think every object has every attribute
         if typ is object and attr == "__getattribute__":


Test patch:
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index e0a80b1..39100e0 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -43,10 +43,14 @@ class TestTypeshedClient(TestNameCheckVisitorBase):
     @assert_passes()
     def test_types(self):
         import math
+        from typing import Container
 
         assert_is_value(math.exp(1.0), TypedValue(float))
         assert_is_value("".isspace(), TypedValue(bool))
 
+        def capybara(x: Container[int]) -> None:
+            assert_is_value(x.__contains__(1), TypedValue(bool))
+
     @assert_passes()
     def test_dict_update(self):
         def capybara():