Repository: quora/pyanalyze
Pull request #617 (2023-05-02 05:05:08)

Improve support for Cythonized methods


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index b647018..26c32d3 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Improve support for Cythonized methods (#617)
+- Add support for the PEP 698 `@override` decorator (#614)
 - Add support for `__new__` methods returning `typing.Self`, fixing
   various failures with the latest release of `typeshed-client` (#615)
 - Add support for importing stub-only modules in other stubs (#615)
@@ -13,7 +15,6 @@
 - Fix bug where objects with a `__call__` method that takes `*args` instead
   of `self` were not considered callable (#600)
 - Better typechecking support for async generators (#594)
-- Add support for the PEP 698 `@override` decorator (#614)
 
 ## Version 0.9.0 (January 16, 2023)
 
diff --git a/pyanalyze/attributes.py b/pyanalyze/attributes.py
index 0bf9df3..0e418f0 100644
--- a/pyanalyze/attributes.py
+++ b/pyanalyze/attributes.py
@@ -362,6 +362,9 @@ def _unwrap_value_from_typed(result: Value, typ: type, ctx: AttrContext) -> Valu
     elif asynq.is_async_fn(cls_val):
         # static or class method
         return result
+    elif _static_hasattr(cls_val, "func_code"):
+        # Cython function probably
+        return UnboundMethodValue(ctx.attr, ctx.root_composite, typevars=typevars)
     elif _static_hasattr(cls_val, "__get__"):
         typeshed_type = ctx.get_attribute_from_typeshed(typ, on_class=False)
         if typeshed_type is not UNINITIALIZED_VALUE:
@@ -417,6 +420,8 @@ def _get_attribute_from_known(obj: object, ctx: AttrContext) -> Value:
 def _get_attribute_from_unbound(
     root_value: UnboundMethodValue, ctx: AttrContext
 ) -> Value:
+    if root_value.secondary_attr_name is not None:
+        return AnyValue(AnySource.inference)
     method = root_value.get_method()
     if method is None:
         return AnyValue(AnySource.inference)
diff --git a/pyanalyze/yield_checker.py b/pyanalyze/yield_checker.py
index 943d990..d07c223 100644
--- a/pyanalyze/yield_checker.py
+++ b/pyanalyze/yield_checker.py
@@ -299,7 +299,7 @@ class YieldChecker:
             return
 
         duplicate_indices = {}  # index to first index
-        seen = {}  # ast.dump result to index
+        seen: Dict[str, int] = {}  # ast.dump result to index
         for i, member in enumerate(node.value.elts):
             # identical AST nodes don't compare equally, so just stringify them for comparison
             code = ast.dump(member)
@@ -620,7 +620,7 @@ class YieldChecker:
 
 def _camel_case_to_snake_case(s: str) -> str:
     """Converts a CamelCase string to snake_case."""
-    out = []
+    out: List[str] = []
     last_was_uppercase = False
     for c in s:
         if c.isupper():


Test patch:
diff --git a/pyanalyze/test_attributes.py b/pyanalyze/test_attributes.py
index 5a970a6..006e1ec 100644
--- a/pyanalyze/test_attributes.py
+++ b/pyanalyze/test_attributes.py
@@ -301,6 +301,19 @@ class TestAttributes(TestNameCheckVisitorBase):
         def capybara():
             assert_is_value(has_prop.does_it_really, AnyValue(AnySource.inference))
 
+    @assert_passes()
+    def test_cached_per_instance(self):
+        from qcore.caching import cached_per_instance
+
+        class C:
+            @cached_per_instance()
+            def f(self) -> int:
+                return 42
+
+        def capybara():
+            c = C()
+            assert_is_value(c.f(), TypedValue(int))
+
 
 class TestHasAttrExtension(TestNameCheckVisitorBase):
     @assert_passes()