From f7790f9d89b01a57407bc645b50bf57b9075f02d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 18 Mar 2023 18:59:06 +0300 Subject: [PATCH 1/2] [stubtest] Respect `@final` runtime decorator, enforce it in stubs --- mypy/stubtest.py | 10 ++++++++++ mypy/test/teststubtest.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index df1c246a82d6f..b74005ccb6193 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -419,6 +419,16 @@ class SubClass(runtime): # type: ignore[misc] # Examples: ctypes.Array, ctypes._SimpleCData pass + # Runtime class might be annotated with `@final`: + if getattr(runtime, "__final__", False) 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] diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 6bb4dfb2c937d..d39812b5f9b6b 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( From f118b722f7de8e71a78195614b3e5641d492f688 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 19 Mar 2023 15:56:51 +0300 Subject: [PATCH 2/2] Address review --- mypy/stubtest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index b74005ccb6193..b0ef94e62480d 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -420,7 +420,12 @@ class SubClass(runtime): # type: ignore[misc] pass # Runtime class might be annotated with `@final`: - if getattr(runtime, "__final__", False) and not stub.is_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", @@ -1349,7 +1354,7 @@ def verify_typealias( "__origin__", "__args__", "__orig_bases__", - "__final__", + "__final__", # Has a specialized check # Consider removing __slots__? "__slots__", }