From 6af70272d4df369fd29096a847e867a9ad2039bc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 1 Nov 2024 08:33:36 +0100 Subject: [PATCH] Add `testDeprecatedOverriddenMethod` and implement the necessary changes in a prototype manner for first discussions. --- mypy/checker.py | 90 ++++++++++++++++------------ test-data/unit/check-deprecated.test | 73 ++++++++++++++++++++++ 2 files changed, 125 insertions(+), 38 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a650bdf2a639..57678291e369 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1981,15 +1981,18 @@ def check_method_override( "__post_init__", ) and (self.options.check_untyped_defs or not defn.is_dynamic()) found_method_base_classes: list[TypeInfo] = [] - for base in defn.info.mro[1:]: - result = self.check_method_or_accessor_override_for_base( - defn, base, check_override_compatibility - ) - if result is None: - # Node was deferred, we will have another attempt later. - return None - if result: - found_method_base_classes.append(base) + for directbase in defn.info.bases: + first_baseclass = True + for base in directbase.type.mro: + result = self.check_method_or_accessor_override_for_base( + defn, base, check_override_compatibility, first_baseclass + ) + if result is None: + # Node was deferred, we will have another attempt later. + return None + if result: + found_method_base_classes.append(base) + first_baseclass = False return found_method_base_classes def check_method_or_accessor_override_for_base( @@ -1997,6 +2000,7 @@ def check_method_or_accessor_override_for_base( defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo, check_override_compatibility: bool, + first_baseclass: bool, ) -> bool | None: """Check if method definition is compatible with a base class. @@ -2020,7 +2024,7 @@ def check_method_or_accessor_override_for_base( if check_override_compatibility: # Check compatibility of the override signature # (__init__, __new__, __init_subclass__ are special). - if self.check_method_override_for_base_with_name(defn, name, base): + if self.check_method_override_for_base_with_name(defn, name, base, first_baseclass): return None if name in operators.inplace_operator_methods: # Figure out the name of the corresponding operator method. @@ -2029,12 +2033,12 @@ def check_method_or_accessor_override_for_base( # always introduced safely if a base class defined __add__. # TODO can't come up with an example where this is # necessary; now it's "just in case" - if self.check_method_override_for_base_with_name(defn, method, base): + if self.check_method_override_for_base_with_name(defn, method, base, first_baseclass): return None return found_base_method def check_method_override_for_base_with_name( - self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo + self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo, first_baseclass: bool ) -> bool: """Check if overriding an attribute `name` of `base` with `defn` is valid. @@ -2135,33 +2139,43 @@ def check_method_override_for_base_with_name( if isinstance(original_type, AnyType) or isinstance(typ, AnyType): pass elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike): - # Check that the types are compatible. - ok = self.check_override( - typ, - original_type, - defn.name, - name, - base.name, - original_class_or_static, - override_class_or_static, - context, - ) - # Check if this override is covariant. - if ( - ok - and original_node - and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes - and self.is_writable_attribute(original_node) - and not is_subtype(original_type, typ, ignore_pos_arg_names=True) - ): - base_str, override_str = format_type_distinctly( - original_type, typ, options=self.options - ) - msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( - f' (base class "{base.name}" defined the type as {base_str},' - f" override has type {override_str})" + if isinstance(original_node, Decorator): + deprecated = original_node.func.deprecated + elif isinstance(original_node, OverloadedFuncDef): + deprecated = original_node.deprecated + else: + deprecated = None + if deprecated is None: + # Check that the types are compatible. + ok = self.check_override( + typ, + original_type, + defn.name, + name, + base.name, + original_class_or_static, + override_class_or_static, + context, ) - self.fail(msg, context) + # Check if this override is covariant. + if ( + ok + and original_node + and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes + and self.is_writable_attribute(original_node) + and not is_subtype(original_type, typ, ignore_pos_arg_names=True) + ): + base_str, override_str = format_type_distinctly( + original_type, typ, options=self.options + ) + msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( + f' (base class "{base.name}" defined the type as {base_str},' + f" override has type {override_str})" + ) + self.fail(msg, context) + elif context.is_explicit_override and first_baseclass and not is_private(context.name): + warn = self.fail if self.options.report_deprecated_as_error else self.note + warn(deprecated, context, code=codes.DEPRECATED) elif isinstance(original_type, UnionType) and any( is_subtype(typ, orig_typ, ignore_pos_arg_names=True) for orig_typ in original_type.items diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index fbfdfcce5a14..dc6aa5494acd 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -542,3 +542,76 @@ h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: def h(x: str) -> str [builtins fixtures/tuple.pyi] + + +[case testDeprecatedOverriddenMethod] + +from typing_extensions import deprecated, override + +class A: + @deprecated("replaced by g1") + def f1(self) -> int: ... + @deprecated("replaced by g2") + def f2(self) -> int: ... + @deprecated("replaced by g3") + def __f3__(self) -> int: ... + @deprecated("replaced by g4") + def __f4__(self) -> int: ... + +# no notes about incompatibilities +# overriding a deprecated method is like defining a new one +class B1(A): + def f1(self) -> int: ... + def f2(self) -> str: ... + def __f3(self) -> int: ... + def __f4(self) -> str: ... + +# no notes about deprecations +# the directly overriden class is okay +class B2(B1): + @override + def f1(self) -> int: ... + @override + def f2(self) -> str: ... + @override + def __f3(self) -> int: ... + @override + def __f4(self) -> str: ... + +# deprecation notes +# single inheritance +class C(A): + @override + def f1(self) -> int: ... # N: function __main__.A.f1 is deprecated: replaced by g1 + @override + def f2(self) -> str: ... # N: function __main__.A.f2 is deprecated: replaced by g2 + @override + def __f3(self) -> int: ... # E: Method "__f3" is marked as an override, but no base method was found with this name + @override + def __f4(self) -> str: ... # E: Method "__f4" is marked as an override, but no base method was found with this name + +# deprecation notes +# multiple inheritance +class D(B1, A): + @override + def f1(self) -> int: ... # N: function __main__.A.f1 is deprecated: replaced by g1 + @override + def f2(self) -> str: ... # N: function __main__.A.f2 is deprecated: replaced by g2 + @override + def __f3(self) -> int: ... # No error? + @override + def __f4(self) -> str: ... # No error? + +# no deprecation notes +# multiple inheritance +class E(B1, C): + @override + def f1(self) -> int: ... + @override + def f2(self) -> str: ... + @override + def __f3(self) -> int: ... # No error? + @override + def __f4(self) -> str: ... # No error? + +[builtins fixtures/tuple.pyi]