diff --git a/mypy/stubtest.py b/mypy/stubtest.py index df1c246a82d6..b0ef94e62480 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -419,6 +419,21 @@ class SubClass(runtime): # type: ignore[misc] # Examples: ctypes.Array, ctypes._SimpleCData pass + # Runtime class might be annotated with `@final`: + try: + runtime_final = getattr(runtime, "__final__", False) + except Exception: + runtime_final = False + + if runtime_final and not stub.is_final: + yield Error( + object_path, + "has `__final__` attribute, but isn't marked with @final in the stub", + stub, + runtime, + stub_desc=repr(stub), + ) + def _verify_metaclass( stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str] @@ -1339,7 +1354,7 @@ def verify_typealias( "__origin__", "__args__", "__orig_bases__", - "__final__", + "__final__", # Has a specialized check # Consider removing __slots__? "__slots__", } diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 6bb4dfb2c937..d39812b5f9b6 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1139,6 +1139,45 @@ def test_not_subclassable(self) -> Iterator[Case]: error="CannotBeSubclassed", ) + @collect_cases + def test_has_runtime_final_decorator(self) -> Iterator[Case]: + yield Case( + stub="from typing_extensions import final", + runtime="from typing_extensions import final", + error=None, + ) + yield Case( + stub=""" + @final + class A: ... + """, + runtime=""" + @final + class A: ... + """, + error=None, + ) + yield Case( # Runtime can miss `@final` decorator + stub=""" + @final + class B: ... + """, + runtime=""" + class B: ... + """, + error=None, + ) + yield Case( # Stub cannot miss `@final` decorator + stub=""" + class C: ... + """, + runtime=""" + @final + class C: ... + """, + error="C", + ) + @collect_cases def test_name_mangling(self) -> Iterator[Case]: yield Case(