Repository: python/mypy

Issue:
[stubgen] `functools.cached_property` is not supported
After https://github.com/python/mypy/issues/8913 mypy can understand that `@cached_property` is a `property`.

So, I think that `stubgen` must reflect that. Right now if you run `stubgen` on this code:

```python
from functools import cached_property

class A:
    @cached_property
    def x(self):
        return 'x'
```

You will get:

```python
class A:
    def x(self): ...
```

This is incorrect, because it turns a property into a method.

But, ideally you should get:

```python
from functools import cached_property

class A:
    @cached_property
    def x(self): ...
```

I already have a working prototype of the solution. Please, feel free to comment on the PR :)


Discussion:

Patch:
diff --git a/mypy/stubgen.py b/mypy/stubgen.py
--- a/mypy/stubgen.py
+++ b/mypy/stubgen.py
@@ -849,6 +849,9 @@ def process_name_expr_decorator(self, expr: NameExpr, context: Decorator) -> tup
             self.add_decorator("property")
             self.add_decorator("abc.abstractmethod")
             is_abstract = True
+        elif self.refers_to_fullname(name, "functools.cached_property"):
+            self.import_tracker.require_name(name)
+            self.add_decorator(name)
         elif self.refers_to_fullname(name, OVERLOAD_NAMES):
             self.add_decorator(name)
             self.add_typing_import("overload")
@@ -894,6 +897,14 @@ def process_member_expr_decorator(
                 self.import_tracker.require_name(expr.expr.name)
                 self.add_decorator(f"{expr.expr.name}.{expr.name}")
             is_abstract = True
+        elif expr.name == "cached_property" and isinstance(expr.expr, NameExpr):
+            explicit_name = expr.expr.name
+            reverse = self.import_tracker.reverse_alias.get(explicit_name)
+            if reverse == "functools" or (reverse is None and explicit_name == "functools"):
+                if reverse is not None:
+                    self.import_tracker.add_import(reverse, alias=explicit_name)
+                self.import_tracker.require_name(explicit_name)
+                self.add_decorator(f"{explicit_name}.{expr.name}")
         elif expr.name == "coroutine":
             if (
                 isinstance(expr.expr, MemberExpr)


Test patch:
diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test
--- a/test-data/unit/stubgen.test
+++ b/test-data/unit/stubgen.test
@@ -319,6 +319,62 @@ class A:
     def f(self, x) -> None: ...
     def h(self) -> None: ...
 
+[case testFunctoolsCachedProperty]
+import functools
+
+class A:
+    @functools.cached_property
+    def x(self):
+        return 'x'
+[out]
+import functools
+
+class A:
+    @functools.cached_property
+    def x(self): ...
+
+[case testFunctoolsCachedPropertyAlias]
+import functools as ft
+
+class A:
+    @ft.cached_property
+    def x(self):
+        return 'x'
+[out]
+import functools as ft
+
+class A:
+    @ft.cached_property
+    def x(self): ...
+
+[case testCachedProperty]
+from functools import cached_property
+
+class A:
+    @cached_property
+    def x(self):
+        return 'x'
+[out]
+from functools import cached_property
+
+class A:
+    @cached_property
+    def x(self): ...
+
+[case testCachedPropertyAlias]
+from functools import cached_property as cp
+
+class A:
+    @cp
+    def x(self):
+        return 'x'
+[out]
+from functools import cached_property as cp
+
+class A:
+    @cp
+    def x(self): ...
+
 [case testStaticMethod]
 class A:
     @staticmethod