Skip to content

Commit

Permalink
Allow union-with-callable attributes to be overridden by methods (#18018
Browse files Browse the repository at this point in the history
)

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
```
  • Loading branch information
brianschubert authored Oct 29, 2024
1 parent 06a566b commit c7c4288
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 14 deletions.
20 changes: 20 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2146,6 +2146,7 @@ def check_method_override_for_base_with_name(
override_class_or_static,
context,
)
# Check if this override is covariant.
if (
ok
and original_node
Expand All @@ -2161,6 +2162,25 @@ def check_method_override_for_base_with_name(
f" override has type {override_str})"
)
self.fail(msg, context)
elif isinstance(original_type, UnionType) and any(
is_subtype(typ, orig_typ, ignore_pos_arg_names=True)
for orig_typ in original_type.items
):
# This method is a subtype of at least one union variant.
if (
original_node
and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes
and self.is_writable_attribute(original_node)
):
# Covariant override of mutable attribute.
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 is_equivalent(original_type, typ):
# Assume invariance for a non-callable attribute here. Note
# that this doesn't affect read-only properties which can have
Expand Down
78 changes: 64 additions & 14 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ class Base:
class Derived(Base):
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[Base], int]")


[case testOverridePartialAttributeWithMethod]
# This was crashing: https://github.com/python/mypy/issues/11686.
class Base:
Expand Down Expand Up @@ -731,19 +730,6 @@ class B(A):
pass
[builtins fixtures/classmethod.pyi]

[case testOverrideCallableAttributeWithSettableProperty]
from typing import Callable

class A:
f: Callable[[str], None]

class B(A):
@property
def f(self) -> Callable[[object], None]: pass
@func.setter
def f(self, x: object) -> None: pass
[builtins fixtures/property.pyi]

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

[case testOverrideCallableAttributeWithSettableProperty]
from typing import Callable

class A:
f: Callable[[str], None]

class B(A):
@property
def f(self) -> Callable[[object], None]: pass
@func.setter
def f(self, x: object) -> None: pass
[builtins fixtures/property.pyi]

[case testOverrideCallableAttributeWithSettablePropertyMutableOverride]
# flags: --enable-error-code=mutable-override
from typing import Callable
Expand All @@ -777,6 +776,57 @@ class B(A):
def f(self, x: object) -> None: pass
[builtins fixtures/property.pyi]

[case testOverrideCallableUnionAttributeWithMethod]
from typing import Callable, Union

class A:
f1: Union[Callable[[str], str], str]
f2: Union[Callable[[str], str], str]
f3: Union[Callable[[str], str], str]
f4: Union[Callable[[str], str], str]

class B(A):
def f1(self, x: str) -> str:
pass

def f2(self, x: object) -> str:
pass

@classmethod
def f3(cls, x: str) -> str:
pass

@staticmethod
def f4(x: str) -> str:
pass
[builtins fixtures/classmethod.pyi]

[case testOverrideCallableUnionAttributeWithMethodMutableOverride]
# flags: --enable-error-code=mutable-override
from typing import Callable, Union

class A:
f1: Union[Callable[[str], str], str]
f2: Union[Callable[[str], str], str]
f3: Union[Callable[[str], str], str]
f4: Union[Callable[[str], str], str]

class B(A):
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]")
pass

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]")
pass

@classmethod
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]")
pass

@staticmethod
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]")
pass
[builtins fixtures/classmethod.pyi]

-- Constructors
-- ------------

Expand Down

0 comments on commit c7c4288

Please sign in to comment.