diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 86f1a63c458bc..c7472af8f2bb0 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -529,8 +529,21 @@ def verify_typeinfo( yield from verify(stub_to_verify, runtime_attr, object_path + [entry]) +def _static_lookup_runtime(object_path: list[str]) -> MaybeMissing[Any]: + static_runtime = importlib.import_module(object_path[0]) + for entry in object_path[1:]: + try: + static_runtime = inspect.getattr_static(static_runtime, entry) + except AttributeError: + # This can happen with mangled names, ignore for now. + # TODO: pass more information about ancestors of nodes/objects to verify, so we don't + # have to do this hacky lookup. Would be useful in several places. + return MISSING + return static_runtime + + def _verify_static_class_methods( - stub: nodes.FuncBase, runtime: Any, object_path: list[str] + stub: nodes.FuncBase, runtime: Any, static_runtime: MaybeMissing[Any], object_path: list[str] ) -> Iterator[str]: if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"): # Special cased by Python, so don't bother checking @@ -545,16 +558,8 @@ def _verify_static_class_methods( yield "stub is a classmethod but runtime is not" return - # Look the object up statically, to avoid binding by the descriptor protocol - static_runtime = importlib.import_module(object_path[0]) - for entry in object_path[1:]: - try: - static_runtime = inspect.getattr_static(static_runtime, entry) - except AttributeError: - # This can happen with mangled names, ignore for now. - # TODO: pass more information about ancestors of nodes/objects to verify, so we don't - # have to do this hacky lookup. Would be useful in a couple other places too. - return + if static_runtime is MISSING: + return if isinstance(static_runtime, classmethod) and not stub.is_class: yield "runtime is a classmethod but stub is not" @@ -945,11 +950,16 @@ def verify_funcitem( if not callable(runtime): return + # Look the object up statically, to avoid binding by the descriptor protocol + static_runtime = _static_lookup_runtime(object_path) + if isinstance(stub, nodes.FuncDef): for error_text in _verify_abstract_status(stub, runtime): yield Error(object_path, error_text, stub, runtime) + for error_text in _verify_final_method(stub, runtime, static_runtime): + yield Error(object_path, error_text, stub, runtime) - for message in _verify_static_class_methods(stub, runtime, object_path): + for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) signature = safe_inspect_signature(runtime) @@ -1063,9 +1073,15 @@ def verify_overloadedfuncdef( for msg in _verify_abstract_status(first_part.func, runtime): yield Error(object_path, msg, stub, runtime) - for message in _verify_static_class_methods(stub, runtime, object_path): + # Look the object up statically, to avoid binding by the descriptor protocol + static_runtime = _static_lookup_runtime(object_path) + + for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) + # TODO: Should call _verify_final_method here, + # but overloaded final methods in stubs cause a stubtest crash: see #14950 + signature = safe_inspect_signature(runtime) if not signature: return @@ -1126,6 +1142,7 @@ def verify_paramspecexpr( def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]: assert stub.func.is_property if isinstance(runtime, property): + yield from _verify_final_method(stub.func, runtime.fget, MISSING) return if inspect.isdatadescriptor(runtime): # It's enough like a property... @@ -1154,6 +1171,17 @@ def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]: yield f"is inconsistent, runtime {item_type} is abstract but stub is not" +def _verify_final_method( + stub: nodes.FuncDef, runtime: Any, static_runtime: MaybeMissing[Any] +) -> Iterator[str]: + if stub.is_final: + return + if getattr(runtime, "__final__", False) or ( + static_runtime is not MISSING and getattr(static_runtime, "__final__", False) + ): + yield "is decorated with @final at runtime, but not in the stub" + + def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None: """Returns a FuncItem that corresponds to the output of the decorator. diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index c909a0bf9c8df..6576618cc4ef2 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1143,7 +1143,10 @@ def test_not_subclassable(self) -> Iterator[Case]: def test_has_runtime_final_decorator(self) -> Iterator[Case]: yield Case( stub="from typing_extensions import final", - runtime="from typing_extensions import final", + runtime=""" + import functools + from typing_extensions import final + """, error=None, ) yield Case( @@ -1177,6 +1180,221 @@ class C: ... """, error="C", ) + yield Case( + stub=""" + class D: + @final + def foo(self) -> None: ... + @final + @staticmethod + def bar() -> None: ... + @staticmethod + @final + def bar2() -> None: ... + @final + @classmethod + def baz(cls) -> None: ... + @classmethod + @final + def baz2(cls) -> None: ... + @property + @final + def eggs(self) -> int: ... + @final + @property + def eggs2(self) -> int: ... + @final + def ham(self, obj: int) -> int: ... + """, + runtime=""" + class D: + @final + def foo(self): pass + @final + @staticmethod + def bar(): pass + @staticmethod + @final + def bar2(): pass + @final + @classmethod + def baz(cls): pass + @classmethod + @final + def baz2(cls): pass + @property + @final + def eggs(self): return 42 + @final + @property + def eggs2(self): pass + @final + @functools.lru_cache() + def ham(self, obj): return obj * 2 + """, + error=None, + ) + # Stub methods are allowed to have @final even if the runtime doesn't... + yield Case( + stub=""" + class E: + @final + def foo(self) -> None: ... + @final + @staticmethod + def bar() -> None: ... + @staticmethod + @final + def bar2() -> None: ... + @final + @classmethod + def baz(cls) -> None: ... + @classmethod + @final + def baz2(cls) -> None: ... + @property + @final + def eggs(self) -> int: ... + @final + @property + def eggs2(self) -> int: ... + @final + def ham(self, obj: int) -> int: ... + """, + runtime=""" + class E: + def foo(self): pass + @staticmethod + def bar(): pass + @staticmethod + def bar2(): pass + @classmethod + def baz(cls): pass + @classmethod + def baz2(cls): pass + @property + def eggs(self): return 42 + @property + def eggs2(self): return 42 + @functools.lru_cache() + def ham(self, obj): return obj * 2 + """, + error=None, + ) + # ...But if the runtime has @final, the stub must have it as well + yield Case( + stub=""" + class F: + def foo(self) -> None: ... + """, + runtime=""" + class F: + @final + def foo(self): pass + """, + error="F.foo", + ) + yield Case( + stub=""" + class G: + @staticmethod + def foo() -> None: ... + """, + runtime=""" + class G: + @final + @staticmethod + def foo(): pass + """, + error="G.foo", + ) + yield Case( + stub=""" + class H: + @staticmethod + def foo() -> None: ... + """, + runtime=""" + class H: + @staticmethod + @final + def foo(): pass + """, + error="H.foo", + ) + yield Case( + stub=""" + class I: + @classmethod + def foo(cls) -> None: ... + """, + runtime=""" + class I: + @final + @classmethod + def foo(cls): pass + """, + error="I.foo", + ) + yield Case( + stub=""" + class J: + @classmethod + def foo(cls) -> None: ... + """, + runtime=""" + class J: + @classmethod + @final + def foo(cls): pass + """, + error="J.foo", + ) + yield Case( + stub=""" + class K: + @property + def foo(self) -> int: ... + """, + runtime=""" + class K: + @property + @final + def foo(self): return 42 + """, + error="K.foo", + ) + # This test wouldn't pass, + # because the runtime can't set __final__ on instances of builtins.property, + # so stubtest has non way of knowing that the runtime was decorated with @final: + # + # yield Case( + # stub=""" + # class K2: + # @property + # def foo(self) -> int: ... + # """, + # runtime=""" + # class K2: + # @final + # @property + # def foo(self): return 42 + # """, + # error="K2.foo", + # ) + yield Case( + stub=""" + class L: + def foo(self, obj: int) -> int: ... + """, + runtime=""" + class L: + @final + @functools.lru_cache() + def foo(self, obj): return obj * 2 + """, + error="L.foo", + ) @collect_cases def test_name_mangling(self) -> Iterator[Case]: