From 1725a129d9632972ac779950292ab561b22527eb Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 03:22:47 +0200 Subject: [PATCH 1/3] Reject @override on dynamic methods not in superclass --- mypy/checker.py | 2 +- test-data/unit/check-functions.test | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7d0b41c516e1e..bf6218d226e83 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5285,7 +5285,7 @@ def visit_decorator_inner( # For overloaded functions/properties we already checked override for overload as a whole. if allow_empty or skip_first_item: return - if e.func.info and not e.func.is_dynamic() and not e.is_overload: + if e.func.info and not e.is_overload: found_method_base_classes = self.check_method_override(e) if ( e.func.is_explicit_override diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index bd59dfbdfd5ee..94df0e3c54356 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3285,6 +3285,17 @@ class C(B): def __f(self, y: int) -> str: pass # OK [typing fixtures/typing-override.pyi] +[case testOverrideUntypedDef] +# flags: --python-version 3.12 +from typing import override + +class Parent: pass + +class Child(Parent): + @override + def foo(self, y): pass # E: Method "foo" is marked as an override, but no base method was found with this name +[typing fixtures/typing-override.pyi] + [case testCallableProperty] from typing import Callable From 1042e696458fe842ce15bfe8b8db7dae8ae71a94 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 03:43:07 +0200 Subject: [PATCH 2/3] Allow @override on classes with Any fallback --- mypy/checker.py | 6 ++++++ test-data/unit/check-functions.test | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index bf6218d226e83..2d82d74cc197e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -750,6 +750,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: defn.is_explicit_override and not found_method_base_classes and found_method_base_classes is not None + # If the class has Any fallback, we can't be certain that a method + # is really missing - it might come from unfollowed import. + and not defn.info.fallback_to_any ): self.msg.no_overridable_method(defn.name, defn) self.check_explicit_override_decorator(defn, found_method_base_classes, defn.impl) @@ -5291,6 +5294,9 @@ def visit_decorator_inner( e.func.is_explicit_override and not found_method_base_classes and found_method_base_classes is not None + # If the class has Any fallback, we can't be certain that a method + # is really missing - it might come from unfollowed import. + and not e.func.info.fallback_to_any ): self.msg.no_overridable_method(e.func.name, e.func) self.check_explicit_override_decorator(e.func, found_method_base_classes) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 94df0e3c54356..ac93c6c203545 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3294,6 +3294,28 @@ class Parent: pass class Child(Parent): @override def foo(self, y): pass # E: Method "foo" is marked as an override, but no base method was found with this name + +[typing fixtures/typing-override.pyi] + +[case testOverrideOnUnknownBaseClass] +# flags: --python-version 3.12 +from typing import overload, override + +from unknown import UnknownParent # type: ignore[import-not-found] + +class UnknownChild(UnknownParent): + @override + def foo(self, y): pass # OK + @override + def bar(self, y: str) -> None: pass # OK + + @override + @overload + def baz(self, y: str) -> None: ... + @override + @overload + def baz(self, y: int) -> None: ... + def baz(self, y: str | int) -> None: ... [typing fixtures/typing-override.pyi] [case testCallableProperty] From 92e750e2fe04483ebe60b951dae814726480eec4 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 17:57:10 +0200 Subject: [PATCH 3/3] Add testcase checking superclass compat --- test-data/unit/check-classes.test | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 65a6a0c9c0a8e..e585a35595cc2 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -6651,7 +6651,51 @@ from typing import TypeVar, Tuple, Callable T = TypeVar('T') def deco(f: Callable[..., T]) -> Callable[..., Tuple[T, int]]: ... [builtins fixtures/tuple.pyi] -[out] + +[case testOverrideWithUntypedNotChecked] +class Parent: + def foo(self, x): + ... + def bar(self, x): + ... + def baz(self, x: int) -> str: + return "" + +class Child(Parent): + def foo(self, y): # OK: names not checked + ... + def bar(self, x, y): + ... + def baz(self, x, y): + return "" +[builtins fixtures/tuple.pyi] + +[case testOverrideWithUntypedCheckedWithCheckUntypedDefs] +# flags: --check-untyped-defs +class Parent: + def foo(self, x): + ... + def bar(self, x): + ... + def baz(self, x: int) -> str: + return "" + +class Child(Parent): + def foo(self, y): # OK: names not checked + ... + def bar(self, x, y) -> None: # E: Signature of "bar" incompatible with supertype "Parent" \ + # N: Superclass: \ + # N: def bar(self, x: Any) -> Any \ + # N: Subclass: \ + # N: def bar(self, x: Any, y: Any) -> None + ... + def baz(self, x, y): # E: Signature of "baz" incompatible with supertype "Parent" \ + # N: Superclass: \ + # N: def baz(self, x: int) -> str \ + # N: Subclass: \ + # N: def baz(self, x: Any, y: Any) -> Any + return "" +[builtins fixtures/tuple.pyi] [case testOptionalDescriptorsBinder] from typing import Type, TypeVar, Optional