Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 702 (@deprecated): overriding deprecated methods #18085

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 64 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,9 @@ 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 +2035,18 @@ 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 +2147,47 @@ 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]
Loading