Repository: python/mypy

Issue:
Stubtest crashes on a stub with an overloaded method where the first overload is decorated with `@final`
**Crash Report**

If you run stubtest on a stub with a method like this, stubtest will crash:

```python
from typing import final, overload

class C:
    @overload
    @final
    def foo(self, obj: str) -> int: ...
    @overload
    def foo(self, obj: int) -> str: ...
```

The crash occurs regardless of the order in which you stack the `@final` and `@overload` decorator on the first overload. (PEP 591 specifies that for overloaded definitions in stubs, the `@final` decorator should be placed on the first overload, but doesn't specify which order they should be stacked in.)

**Traceback**

```pytb
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 1823, in <module>
    sys.exit(main())
             ^^^^^^
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 1819, in main
    return test_stubs(parse_options(sys.argv[1:]))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 1692, in test_stubs
    for error in test_module(module):
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 218, in test_module
    yield from verify(stub, runtime, [module_name])
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 396, in verify_mypyfile
    yield from verify(stub_entry, runtime_entry, object_path + [entry])
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 517, in verify_typeinfo
    yield from verify(stub_to_verify, runtime_attr, object_path + [entry])
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 1050, in verify_overloadedfuncdef
    stub_sig = Signature.from_overloadedfuncdef(stub)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alexw\AppData\Local\Temp\tmp_p_nrkoa\Lib\site-packages\mypy\stubtest.py", line 756, in from_overloadedfuncdef
    assert func is not None
           ^^^^^^^^^^^^^^^^
AssertionError
```

**To Reproduce**

Two ways to reproduce:

**(1) Apply this diff, and then run `pytest mypy/test/teststubtest.py`:**

```diff
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
index d39812b5f..b7f114b5e 100644
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -1177,6 +1177,24 @@ class StubtestUnit(unittest.TestCase):
             """,
             error="C",
         )
+        yield Case(
+            stub="""
+            from typing import overload
+            from typing_extensions import final
+            class D:
+                @overload
+                @final
+                def foo(self, obj: int) -> str: ...
+                @overload
+                def foo(self, obj: str) -> int: ...
+            """,
+            runtime="""
+            class D:
+                def foo(self, obj):
+                    return 42 if isinstance(obj, str) else "foo"
+            """,
+            error=None
+        )
```

**(2) With a typeshed clone checked out, apply this diff, then run `python tests/stubtest_stdlib.py`:**

```diff
diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi
index 2c21cd95d..8cdcd3b6a 100644
--- a/stdlib/builtins.pyi
+++ b/stdlib/builtins.pyi
@@ -189,6 +189,7 @@ class type:

 class super:
     @overload
+    @final
     def __init__(self, __t: Any, __obj: Any) -> None: ...
     @overload
     def __init__(self, __t: Any) -> None: ...
```

**Your Environment**

Reproduced with mypy 1.1.1 and mypy @ bfa9eacedb0554e1a6fe9245dbd5ccdbbc555fae



Discussion:

Resolving patch:
diff --git a/mypy/stubtest.py b/mypy/stubtest.py
--- a/mypy/stubtest.py
+++ b/mypy/stubtest.py
@@ -1215,6 +1215,12 @@ def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem
     def apply_decorator_to_funcitem(
         decorator: nodes.Expression, func: nodes.FuncItem
     ) -> nodes.FuncItem | None:
+        if (
+            isinstance(decorator, nodes.CallExpr)
+            and isinstance(decorator.callee, nodes.RefExpr)
+            and decorator.callee.fullname in mypy.types.DEPRECATED_TYPE_NAMES
+        ):
+            return func
         if not isinstance(decorator, nodes.RefExpr):
             return None
         if not decorator.fullname:
@@ -1223,6 +1229,7 @@ def apply_decorator_to_funcitem(
         if (
             decorator.fullname in ("builtins.staticmethod", "abc.abstractmethod")
             or decorator.fullname in mypy.types.OVERLOAD_NAMES
+            or decorator.fullname in mypy.types.FINAL_DECORATOR_NAMES
         ):
             return func
         if decorator.fullname == "builtins.classmethod":
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -119,6 +119,9 @@
 # Supported Annotated type names.
 ANNOTATED_TYPE_NAMES: Final = ("typing.Annotated", "typing_extensions.Annotated")
 
+# Supported @deprecated type names
+DEPRECATED_TYPE_NAMES: Final = ("typing.deprecated", "typing_extensions.deprecated")
+
 # We use this constant in various places when checking `tuple` subtyping:
 TUPLE_LIKE_INSTANCE_NAMES: Final = (
     "builtins.tuple",


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -71,6 +71,8 @@ class Sequence(Iterable[_T_co]): ...
 class Tuple(Sequence[_T_co]): ...
 class NamedTuple(tuple[Any, ...]): ...
 def overload(func: _T) -> _T: ...
+def deprecated(__msg: str) -> Callable[[_T], _T]: ...
+def final(func: _T) -> _T: ...
 """
 
 stubtest_builtins_stub = """
@@ -630,6 +632,23 @@ def f5(__b: str) -> str: ...
             runtime="def f5(x, /): pass",
             error=None,
         )
+        yield Case(
+            stub="""
+            from typing import deprecated, final
+            class Foo:
+                @overload
+                @final
+                def f6(self, __a: int) -> int: ...
+                @overload
+                @deprecated("evil")
+                def f6(self, __b: str) -> str: ...
+            """,
+            runtime="""
+            class Foo:
+                def f6(self, x, /): pass
+            """,
+            error=None,
+        )
 
     @collect_cases
     def test_property(self) -> Iterator[Case]: