Skip to content

Commit 7f09f0c

Browse files
Emit [mutable-override] for covariant override of attribute with method (#18058)
Fixes #18052 Given: ```python # flags: --enable-error-code=mutable-override from typing import Callable class Parent: func: Callable[[str], None] class Child(Parent): def func(self, x: object) -> None: pass ``` Before: ``` Success: no issues found in 1 source file ``` After: ``` main.py:7 error: Covariant override of a mutable attribute (base class "Parent" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]") Found 1 error in 1 file (checked 1 source file) ```
1 parent 80843fe commit 7f09f0c

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

mypy/checker.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,7 +2136,7 @@ def check_method_override_for_base_with_name(
21362136
pass
21372137
elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike):
21382138
# Check that the types are compatible.
2139-
self.check_override(
2139+
ok = self.check_override(
21402140
typ,
21412141
original_type,
21422142
defn.name,
@@ -2146,6 +2146,21 @@ def check_method_override_for_base_with_name(
21462146
override_class_or_static,
21472147
context,
21482148
)
2149+
if (
2150+
ok
2151+
and original_node
2152+
and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes
2153+
and self.is_writable_attribute(original_node)
2154+
and not is_subtype(original_type, typ, ignore_pos_arg_names=True)
2155+
):
2156+
base_str, override_str = format_type_distinctly(
2157+
original_type, typ, options=self.options
2158+
)
2159+
msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg(
2160+
f' (base class "{base.name}" defined the type as {base_str},'
2161+
f" override has type {override_str})"
2162+
)
2163+
self.fail(msg, context)
21492164
elif is_equivalent(original_type, typ):
21502165
# Assume invariance for a non-callable attribute here. Note
21512166
# that this doesn't affect read-only properties which can have
@@ -2235,7 +2250,7 @@ def check_override(
22352250
original_class_or_static: bool,
22362251
override_class_or_static: bool,
22372252
node: Context,
2238-
) -> None:
2253+
) -> bool:
22392254
"""Check a method override with given signatures.
22402255
22412256
Arguments:
@@ -2385,6 +2400,7 @@ def erase_override(t: Type) -> Type:
23852400
node,
23862401
code=codes.OVERRIDE,
23872402
)
2403+
return not fail
23882404

23892405
def check__exit__return_type(self, defn: FuncItem) -> None:
23902406
"""Generate error if the return type of __exit__ is problematic.

test-data/unit/check-classes.test

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,72 @@ class B(A):
710710
@dec
711711
def f(self) -> Y: pass
712712

713+
[case testOverrideCallableAttributeWithMethod]
714+
from typing import Callable
715+
716+
class A:
717+
f1: Callable[[str], None]
718+
f2: Callable[[str], None]
719+
f3: Callable[[str], None]
720+
721+
class B(A):
722+
def f1(self, x: object) -> None:
723+
pass
724+
725+
@classmethod
726+
def f2(cls, x: object) -> None:
727+
pass
728+
729+
@staticmethod
730+
def f3(x: object) -> None:
731+
pass
732+
[builtins fixtures/classmethod.pyi]
733+
734+
[case testOverrideCallableAttributeWithSettableProperty]
735+
from typing import Callable
736+
737+
class A:
738+
f: Callable[[str], None]
739+
740+
class B(A):
741+
@property
742+
def f(self) -> Callable[[object], None]: pass
743+
@func.setter
744+
def f(self, x: object) -> None: pass
745+
[builtins fixtures/property.pyi]
746+
747+
[case testOverrideCallableAttributeWithMethodMutableOverride]
748+
# flags: --enable-error-code=mutable-override
749+
from typing import Callable
750+
751+
class A:
752+
f1: Callable[[str], None]
753+
f2: Callable[[str], None]
754+
f3: Callable[[str], None]
755+
756+
class B(A):
757+
def f1(self, x: object) -> None: pass # E: Covariant override of a mutable attribute (base class "A" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]")
758+
759+
@classmethod
760+
def f2(cls, x: object) -> None: pass # E: Covariant override of a mutable attribute (base class "A" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]")
761+
762+
@staticmethod
763+
def f3(x: object) -> None: pass # E: Covariant override of a mutable attribute (base class "A" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]")
764+
[builtins fixtures/classmethod.pyi]
765+
766+
[case testOverrideCallableAttributeWithSettablePropertyMutableOverride]
767+
# flags: --enable-error-code=mutable-override
768+
from typing import Callable
769+
770+
class A:
771+
f: Callable[[str], None]
772+
773+
class B(A):
774+
@property # E: Covariant override of a mutable attribute (base class "A" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]")
775+
def f(self) -> Callable[[object], None]: pass
776+
@func.setter
777+
def f(self, x: object) -> None: pass
778+
[builtins fixtures/property.pyi]
713779

714780
-- Constructors
715781
-- ------------

0 commit comments

Comments
 (0)