Skip to content

Commit c7c4288

Browse files
Allow union-with-callable attributes to be overridden by methods (#18018)
Fixes #12569 ### Before ```python from typing import Callable class Parent: f1: Callable[[str], str] f2: Callable[[str], str] | str class Child(Parent): def f1(self, x: str) -> str: return x # ok def f2(self, x: str) -> str: return x # Signature of "f2" incompatible with supertype "Parent" ``` ### After ```python from typing import Callable class Parent: f1: Callable[[str], str] f2: Callable[[str], str] | str class Child(Parent): def f1(self, x: str) -> str: return x # ok def f2(self, x: str) -> str: return x # ok ```
1 parent 06a566b commit c7c4288

File tree

2 files changed

+84
-14
lines changed

2 files changed

+84
-14
lines changed

mypy/checker.py

+20
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,7 @@ def check_method_override_for_base_with_name(
21462146
override_class_or_static,
21472147
context,
21482148
)
2149+
# Check if this override is covariant.
21492150
if (
21502151
ok
21512152
and original_node
@@ -2161,6 +2162,25 @@ def check_method_override_for_base_with_name(
21612162
f" override has type {override_str})"
21622163
)
21632164
self.fail(msg, context)
2165+
elif isinstance(original_type, UnionType) and any(
2166+
is_subtype(typ, orig_typ, ignore_pos_arg_names=True)
2167+
for orig_typ in original_type.items
2168+
):
2169+
# This method is a subtype of at least one union variant.
2170+
if (
2171+
original_node
2172+
and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes
2173+
and self.is_writable_attribute(original_node)
2174+
):
2175+
# Covariant override of mutable attribute.
2176+
base_str, override_str = format_type_distinctly(
2177+
original_type, typ, options=self.options
2178+
)
2179+
msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg(
2180+
f' (base class "{base.name}" defined the type as {base_str},'
2181+
f" override has type {override_str})"
2182+
)
2183+
self.fail(msg, context)
21642184
elif is_equivalent(original_type, typ):
21652185
# Assume invariance for a non-callable attribute here. Note
21662186
# that this doesn't affect read-only properties which can have

test-data/unit/check-classes.test

+64-14
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ class Base:
147147
class Derived(Base):
148148
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[Base], int]")
149149

150-
151150
[case testOverridePartialAttributeWithMethod]
152151
# This was crashing: https://github.com/python/mypy/issues/11686.
153152
class Base:
@@ -731,19 +730,6 @@ class B(A):
731730
pass
732731
[builtins fixtures/classmethod.pyi]
733732

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-
747733
[case testOverrideCallableAttributeWithMethodMutableOverride]
748734
# flags: --enable-error-code=mutable-override
749735
from typing import Callable
@@ -763,6 +749,19 @@ class B(A):
763749
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]")
764750
[builtins fixtures/classmethod.pyi]
765751

752+
[case testOverrideCallableAttributeWithSettableProperty]
753+
from typing import Callable
754+
755+
class A:
756+
f: Callable[[str], None]
757+
758+
class B(A):
759+
@property
760+
def f(self) -> Callable[[object], None]: pass
761+
@func.setter
762+
def f(self, x: object) -> None: pass
763+
[builtins fixtures/property.pyi]
764+
766765
[case testOverrideCallableAttributeWithSettablePropertyMutableOverride]
767766
# flags: --enable-error-code=mutable-override
768767
from typing import Callable
@@ -777,6 +776,57 @@ class B(A):
777776
def f(self, x: object) -> None: pass
778777
[builtins fixtures/property.pyi]
779778

779+
[case testOverrideCallableUnionAttributeWithMethod]
780+
from typing import Callable, Union
781+
782+
class A:
783+
f1: Union[Callable[[str], str], str]
784+
f2: Union[Callable[[str], str], str]
785+
f3: Union[Callable[[str], str], str]
786+
f4: Union[Callable[[str], str], str]
787+
788+
class B(A):
789+
def f1(self, x: str) -> str:
790+
pass
791+
792+
def f2(self, x: object) -> str:
793+
pass
794+
795+
@classmethod
796+
def f3(cls, x: str) -> str:
797+
pass
798+
799+
@staticmethod
800+
def f4(x: str) -> str:
801+
pass
802+
[builtins fixtures/classmethod.pyi]
803+
804+
[case testOverrideCallableUnionAttributeWithMethodMutableOverride]
805+
# flags: --enable-error-code=mutable-override
806+
from typing import Callable, Union
807+
808+
class A:
809+
f1: Union[Callable[[str], str], str]
810+
f2: Union[Callable[[str], str], str]
811+
f3: Union[Callable[[str], str], str]
812+
f4: Union[Callable[[str], str], str]
813+
814+
class B(A):
815+
def f1(self, x: str) -> str: # E: Covariant override of a mutable attribute (base class "A" defined the type as "Union[Callable[[str], str], str]", override has type "Callable[[str], str]")
816+
pass
817+
818+
def f2(self, x: object) -> str: # E: Covariant override of a mutable attribute (base class "A" defined the type as "Union[Callable[[str], str], str]", override has type "Callable[[object], str]")
819+
pass
820+
821+
@classmethod
822+
def f3(cls, x: str) -> str: # E: Covariant override of a mutable attribute (base class "A" defined the type as "Union[Callable[[str], str], str]", override has type "Callable[[str], str]")
823+
pass
824+
825+
@staticmethod
826+
def f4(x: str) -> str: # E: Covariant override of a mutable attribute (base class "A" defined the type as "Union[Callable[[str], str], str]", override has type "Callable[[str], str]")
827+
pass
828+
[builtins fixtures/classmethod.pyi]
829+
780830
-- Constructors
781831
-- ------------
782832

0 commit comments

Comments
 (0)