Repository: quora/pyanalyze
Pull request #402 (2022-01-12 01:01:57)

fix recursive base


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 772d173..6f77751 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Fix stub classes with references to themselves in their
+  base classes, such as `os._ScandirIterator` in typeshed (#402)
 - Fix type narrowing on the `else` case of `issubclass()`
   (#401)
 - Fix indexing a list with an index typed as a
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index b86262c..d95f3b9 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -155,6 +155,9 @@ class TypeshedFinder:
     _attribute_cache: Dict[Tuple[str, str, bool], Value] = field(
         default_factory=dict, repr=False, init=False
     )
+    _active_infos: List[typeshed_client.resolver.ResolvedName] = field(
+        default_factory=list, repr=False, init=False
+    )
 
     @classmethod
     def make(cls, options: Options, *, verbose: bool = False) -> "TypeshedFinder":
@@ -860,7 +863,7 @@ class TypeshedFinder:
 
     def make_synthetic_type(self, module: str, info: typeshed_client.NameInfo) -> Value:
         fq_name = f"{module}.{info.name}"
-        bases = self.get_bases_for_fq_name(fq_name)
+        bases = self._get_bases_from_info(info, module)
         typ = TypedValue(fq_name)
         if bases is not None:
             if any(
@@ -908,6 +911,19 @@ class TypeshedFinder:
 
     def _value_from_info(
         self, info: typeshed_client.resolver.ResolvedName, module: str
+    ) -> Value:
+        # This guard against infinite recursion if a type refers to itself
+        # (real-world example: os._ScandirIterator).
+        if info in self._active_infos:
+            return AnyValue(AnySource.inference)
+        self._active_infos.append(info)
+        try:
+            return self._value_from_info_inner(info, module)
+        finally:
+            self._active_infos.pop()
+
+    def _value_from_info_inner(
+        self, info: typeshed_client.resolver.ResolvedName, module: str
     ) -> Value:
         if isinstance(info, typeshed_client.ImportedInfo):
             return self._value_from_info(info.info, ".".join(info.source_module))
@@ -945,7 +961,7 @@ class TypeshedFinder:
                         return val
                     if info.ast.value:
                         return self._parse_expr(info.ast.value, module)
-                elif isinstance(info.ast, ast.FunctionDef):
+                elif isinstance(info.ast, (ast.FunctionDef, ast.AsyncFunctionDef)):
                     sig = self._get_signature_from_info(info, None, fq_name, module)
                     if sig is not None:
                         return CallableValue(sig)


Test patch:
diff --git a/pyanalyze/stubs/_pyanalyze_tests-stubs/recursion.pyi b/pyanalyze/stubs/_pyanalyze_tests-stubs/recursion.pyi
new file mode 100644
index 0000000..4e1e00f
--- /dev/null
+++ b/pyanalyze/stubs/_pyanalyze_tests-stubs/recursion.pyi
@@ -0,0 +1,4 @@
+from typing import ContextManager, AnyStr
+
+class _ScandirIterator(ContextManager[_ScandirIterator[AnyStr]]):
+    def close(self) -> None: ...
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index 753fe56..1774a15 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -236,6 +236,20 @@ class TestBundledStubs(TestNameCheckVisitorBase):
                 TypedValue(TextIO) | TypedValue(BinaryIO),
             )
 
+    @assert_passes()
+    def test_recursive_base(self):
+        from typing import Any, ContextManager
+
+        def capybara():
+            from _pyanalyze_tests.recursion import _ScandirIterator
+
+            def want_cm(cm: ContextManager[Any]) -> None:
+                pass
+
+            def f(x: _ScandirIterator):
+                want_cm(x)
+                len(x)  # E: incompatible_argument
+
 
 class Parent(Generic[T]):
     pass