diff --git a/mypy/checker.py b/mypy/checker.py index 7e025b07cb99..b578cc5fae26 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -643,15 +643,17 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if defn.impl: defn.impl.accept(self) if defn.info: - found_base_method = self.check_method_override(defn) - if defn.is_explicit_override and found_base_method is False: + method_base_class = self.check_method_override(defn) + if defn.is_explicit_override and not method_base_class: self.msg.no_overridable_method(defn.name, defn) elif ( - found_base_method + method_base_class and self.options.strict_override_decorator and not defn.is_explicit_override ): - self.msg.override_decorator_missing(defn.name, defn.impl or defn) + self.msg.override_decorator_missing( + defn.name, method_base_class.fullname, defn.impl or defn + ) self.check_inplace_operator_method(defn) if not defn.is_property: self.check_overlapping_overloads(defn) @@ -978,13 +980,15 @@ def _visit_func_def(self, defn: FuncDef) -> None: # overload, the legality of the override has already # been typechecked, and decorated methods will be # checked when the decorator is. - found_base_method = self.check_method_override(defn) + method_base_class = self.check_method_override(defn) if ( - found_base_method + method_base_class and self.options.strict_override_decorator and defn.name not in ("__init__", "__new__") ): - self.msg.override_decorator_missing(defn.name, defn) + self.msg.override_decorator_missing( + defn.name, method_base_class.fullname, defn + ) self.check_inplace_operator_method(defn) if defn.original_def: # Override previous definition. @@ -1826,23 +1830,26 @@ def expand_typevars( else: return [(defn, typ)] - def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None: + def check_method_override( + self, defn: FuncDef | OverloadedFuncDef | Decorator + ) -> TypeInfo | None: """Check if function definition is compatible with base classes. This may defer the method if a signature is not available in at least one base class. Return ``None`` if that happens. + Return the first base class if an attribute with the method name was found within it. Return ``True`` if an attribute with the method name was found in the base class. """ # Check against definitions in base classes. - found_base_method = False for base in defn.info.mro[1:]: result = self.check_method_or_accessor_override_for_base(defn, base) if result is None: # Node was deferred, we will have another attempt later. return None - found_base_method |= result - return found_base_method + if result: + return base + return None def check_method_or_accessor_override_for_base( self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo @@ -4752,15 +4759,17 @@ def visit_decorator(self, e: Decorator) -> None: self.check_incompatible_property_override(e) # For overloaded functions we already checked override for overload as a whole. if e.func.info and not e.func.is_dynamic() and not e.is_overload: - found_base_method = self.check_method_override(e) - if e.func.is_explicit_override and found_base_method is False: + method_base_class = self.check_method_override(e) + if e.func.is_explicit_override and not method_base_class: self.msg.no_overridable_method(e.func.name, e.func) elif ( - found_base_method + method_base_class and self.options.strict_override_decorator and not e.func.is_explicit_override ): - self.msg.override_decorator_missing(e.func.name, e.func) + self.msg.override_decorator_missing( + e.func.name, method_base_class.fullname, e.func + ) if e.func.info and e.func.name in ("__init__", "__new__"): if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)): diff --git a/mypy/messages.py b/mypy/messages.py index 34df4a6e62cb..af276c670bf5 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1525,10 +1525,10 @@ def no_overridable_method(self, name: str, context: Context) -> None: context, ) - def override_decorator_missing(self, name: str, context: Context) -> None: + def override_decorator_missing(self, name: str, base_name: str, context: Context) -> None: self.fail( f'Method "{name}" is not marked as override ' - "but is overriding a method in a base class", + f'but is overriding a method in class "{base_name}"', context, ) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index ebd914f27fd7..cd0b30a8f2fd 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3013,7 +3013,10 @@ class B(A): def f(self, y: int) -> str: pass class C(A): - def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in a base class + def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in class "__main__.A" + +class D(B): + def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in class "__main__.B" [typing fixtures/typing-override.pyi] [case requireExplicitOverrideSpecialMethod] @@ -3042,7 +3045,7 @@ class B(A): class C(A): @property - def prop(self) -> int: pass # E: Method "prop" is not marked as override but is overriding a method in a base class + def prop(self) -> int: pass # E: Method "prop" is not marked as override but is overriding a method in class "__main__.A" [typing fixtures/typing-override.pyi] [builtins fixtures/property.pyi] @@ -3070,5 +3073,22 @@ class C(A): def f(self, y: int) -> str: ... @overload def f(self, y: str) -> str: ... - def f(self, y): pass # E: Method "f" is not marked as override but is overriding a method in a base class + def f(self, y): pass # E: Method "f" is not marked as override but is overriding a method in class "__main__.A" +[typing fixtures/typing-override.pyi] + +[case requireExplicitOverrideMultipleInheritance] +# flags: --strict-override-decorator --python-version 3.12 +from typing import override + +class A: + def f(self, x: int) -> str: pass +class B: + def f(self, y: int) -> str: pass + +class C(A, B): + @override + def f(self, z: int) -> str: pass + +class D(A, B): + def f(self, z: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in class "__main__.A" [typing fixtures/typing-override.pyi]