Skip to content

Commit

Permalink
Add testDeprecatedOverriddenMethod and implement the necessary chan…
Browse files Browse the repository at this point in the history
…ges in a prototype manner for first discussions.
  • Loading branch information
tyralla committed Nov 1, 2024
1 parent b59878e commit 6af7027
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 38 deletions.
90 changes: 52 additions & 38 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1981,22 +1981,26 @@ 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(
self,
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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 6af7027

Please sign in to comment.