Skip to content

Commit 3bd6e47

Browse files
authored
Fix covariant overriding of decorated methods (#8350)
Fixes #5836.
1 parent c059437 commit 3bd6e47

File tree

2 files changed

+48
-13
lines changed

2 files changed

+48
-13
lines changed

mypy/checker.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,7 +1439,7 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncDef,
14391439
self.msg.cant_override_final(name, base.name, defn)
14401440
# Second, final can't override anything writeable independently of types.
14411441
if defn.is_final:
1442-
self.check_no_writable(name, base_attr.node, defn)
1442+
self.check_if_final_var_override_writable(name, base_attr.node, defn)
14431443

14441444
# Check the type of override.
14451445
if name not in ('__init__', '__new__', '__init_subclass__'):
@@ -1534,7 +1534,10 @@ def check_method_override_for_base_with_name(
15341534
# that this doesn't affect read-only properties which can have
15351535
# covariant overrides.
15361536
#
1537-
# TODO: Allow covariance for read-only attributes?
1537+
pass
1538+
elif (base_attr.node and not self.is_writable_attribute(base_attr.node)
1539+
and is_subtype(typ, original_type)):
1540+
# If the attribute is read-only, allow covariance
15381541
pass
15391542
else:
15401543
self.msg.signature_incompatible_with_supertype(
@@ -1920,7 +1923,7 @@ class C(B, A[int]): ... # this is unsafe because...
19201923
if is_final_node(second.node):
19211924
self.msg.cant_override_final(name, base2.name, ctx)
19221925
if is_final_node(first.node):
1923-
self.check_no_writable(name, second.node, ctx)
1926+
self.check_if_final_var_override_writable(name, second.node, ctx)
19241927
# __slots__ is special and the type can vary across class hierarchy.
19251928
if name == '__slots__':
19261929
ok = True
@@ -2385,10 +2388,14 @@ def check_compatibility_final_super(self, node: Var,
23852388
self.msg.cant_override_final(node.name, base.name, node)
23862389
return False
23872390
if node.is_final:
2388-
self.check_no_writable(node.name, base_node, node)
2391+
self.check_if_final_var_override_writable(node.name, base_node, node)
23892392
return True
23902393

2391-
def check_no_writable(self, name: str, base_node: Optional[Node], ctx: Context) -> None:
2394+
def check_if_final_var_override_writable(self,
2395+
name: str,
2396+
base_node:
2397+
Optional[Node],
2398+
ctx: Context) -> None:
23922399
"""Check that a final variable doesn't override writeable attribute.
23932400
23942401
This is done to prevent situations like this:
@@ -2400,14 +2407,10 @@ class D(C):
24002407
x: C = D()
24012408
x.attr = 3 # Oops!
24022409
"""
2403-
if isinstance(base_node, Var):
2404-
ok = False
2405-
elif isinstance(base_node, OverloadedFuncDef) and base_node.is_property:
2406-
first_item = cast(Decorator, base_node.items[0])
2407-
ok = not first_item.var.is_settable_property
2408-
else:
2409-
ok = True
2410-
if not ok:
2410+
writable = True
2411+
if base_node:
2412+
writable = self.is_writable_attribute(base_node)
2413+
if writable:
24112414
self.msg.final_cant_override_writable(name, ctx)
24122415

24132416
def get_final_context(self) -> bool:
@@ -4868,6 +4871,16 @@ def conditional_type_map_with_intersection(self,
48684871
new_yes_type = make_simplified_union(out)
48694872
return {expr: new_yes_type}, {}
48704873

4874+
def is_writable_attribute(self, node: Node) -> bool:
4875+
"""Check if an attribute is writable"""
4876+
if isinstance(node, Var):
4877+
return True
4878+
elif isinstance(node, OverloadedFuncDef) and node.is_property:
4879+
first_item = cast(Decorator, node.items[0])
4880+
return first_item.var.is_settable_property
4881+
else:
4882+
return False
4883+
48714884

48724885
def conditional_type_map(expr: Expression,
48734886
current_type: Optional[Type],

test-data/unit/check-classes.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,28 @@ class B(A):
512512
def h(cls) -> int: pass
513513
[builtins fixtures/classmethod.pyi]
514514

515+
[case testAllowCovarianceInReadOnlyAttributes]
516+
from typing import Callable, TypeVar
517+
518+
T = TypeVar('T')
519+
520+
class X:
521+
pass
522+
523+
524+
class Y(X):
525+
pass
526+
527+
def dec(f: Callable[..., T]) -> T: pass
528+
529+
class A:
530+
@dec
531+
def f(self) -> X: pass
532+
533+
class B(A):
534+
@dec
535+
def f(self) -> Y: pass
536+
515537

516538
-- Constructors
517539
-- ------------

0 commit comments

Comments
 (0)