Skip to content

Commit 319d2a5

Browse files
Patch check_op_reversible to support tuple subclasses. (#19046)
Fixes #19006 - Added unit test `TypeCheckSuite::check-expressions.test::testReverseBinaryOperator4` - Added fixture `tuple-typeshed.pyi` that duplicates the tuple definition from typeshed. This patch drops the `is_subtype(right_type, left_type)` check in favor of the weaker `covers_at_runtime(right_type, left_type)`.
1 parent bc2481a commit 319d2a5

File tree

3 files changed

+97
-7
lines changed

3 files changed

+97
-7
lines changed

mypy/checkexpr.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
from mypy.semanal_enum import ENUM_BASES
121121
from mypy.state import state
122122
from mypy.subtypes import (
123+
covers_at_runtime,
123124
find_member,
124125
is_equivalent,
125126
is_same_type,
@@ -4048,14 +4049,21 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
40484049

40494050
variants_raw = [(op_name, left_op, left_type, right_expr)]
40504051
elif (
4051-
is_subtype(right_type, left_type)
4052-
and isinstance(left_type, Instance)
4053-
and isinstance(right_type, Instance)
4054-
and not (
4055-
left_type.type.alt_promote is not None
4056-
and left_type.type.alt_promote.type is right_type.type
4052+
(
4053+
# Checking (A implies B) using the logically equivalent (not A or B), where
4054+
# A: left and right are both `Instance` objects
4055+
# B: right's __rop__ method is different from left's __op__ method
4056+
not (isinstance(left_type, Instance) and isinstance(right_type, Instance))
4057+
or (
4058+
lookup_definer(left_type, op_name) != lookup_definer(right_type, rev_op_name)
4059+
and (
4060+
left_type.type.alt_promote is None
4061+
or left_type.type.alt_promote.type is not right_type.type
4062+
)
4063+
)
40574064
)
4058-
and lookup_definer(left_type, op_name) != lookup_definer(right_type, rev_op_name)
4065+
# Note: use `covers_at_runtime` instead of `is_subtype` (#19006)
4066+
and covers_at_runtime(right_type, left_type)
40594067
):
40604068
# When we do "A() + B()" where B is a subclass of A, we'll actually try calling
40614069
# B's __radd__ method first, but ONLY if B explicitly defines or overrides the

test-data/unit/check-expressions.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,29 @@ class B:
681681
s: str
682682
s = A() + B() # E: Unsupported operand types for + ("A" and "B")
683683

684+
685+
[case testReverseBinaryOperator4]
686+
from typing import assert_type, Never
687+
688+
class Size(tuple[int, ...]):
689+
def __add__(self, other: tuple[int, ...], /) -> "Size": return Size() # type: ignore[override]
690+
def __radd__(self, other: tuple[int, ...], /) -> "Size": return Size()
691+
692+
size: Size = Size([3, 4])
693+
tup0: tuple[()] = ()
694+
tup1: tuple[int] = (1,)
695+
tup2: tuple[int, int] = (1, 2)
696+
tupN: tuple[int, ...] = (1, 2, 3)
697+
tupX: tuple[Never, ...] = ()
698+
699+
assert_type(tup0 + size, Size)
700+
assert_type(tup1 + size, Size)
701+
assert_type(tup2 + size, Size)
702+
assert_type(tupN + size, Size)
703+
assert_type(tupX + size, Size)
704+
705+
[builtins fixtures/tuple-typeshed.pyi]
706+
684707
[case testBinaryOperatorWithAnyRightOperand]
685708
from typing import Any, cast
686709
class A: pass
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# tuple definition from typeshed,
2+
from typing import (
3+
Generic,
4+
Sequence,
5+
TypeVar,
6+
Iterable,
7+
Iterator,
8+
Any,
9+
overload,
10+
Self,
11+
Protocol,
12+
)
13+
from types import GenericAlias
14+
15+
_T = TypeVar("_T")
16+
_T_co = TypeVar('_T_co', covariant=True)
17+
18+
class tuple(Sequence[_T_co], Generic[_T_co]):
19+
def __new__(cls, iterable: Iterable[_T_co] = ..., /) -> Self: ...
20+
def __len__(self) -> int: ...
21+
def __contains__(self, key: object, /) -> bool: ...
22+
@overload
23+
def __getitem__(self, key: SupportsIndex, /) -> _T_co: ...
24+
@overload
25+
def __getitem__(self, key: slice, /) -> tuple[_T_co, ...]: ...
26+
def __iter__(self) -> Iterator[_T_co]: ...
27+
def __lt__(self, value: tuple[_T_co, ...], /) -> bool: ...
28+
def __le__(self, value: tuple[_T_co, ...], /) -> bool: ...
29+
def __gt__(self, value: tuple[_T_co, ...], /) -> bool: ...
30+
def __ge__(self, value: tuple[_T_co, ...], /) -> bool: ...
31+
def __eq__(self, value: object, /) -> bool: ...
32+
def __hash__(self) -> int: ...
33+
@overload
34+
def __add__(self, value: tuple[_T_co, ...], /) -> tuple[_T_co, ...]: ...
35+
@overload
36+
def __add__(self, value: tuple[_T, ...], /) -> tuple[_T_co | _T, ...]: ...
37+
def __mul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...
38+
def __rmul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...
39+
def count(self, value: Any, /) -> int: ...
40+
def index(self, value: Any, start: SupportsIndex = ..., stop: SupportsIndex = ..., /) -> int: ...
41+
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
42+
43+
class dict: pass
44+
class int: pass
45+
class slice: pass
46+
class bool(int): pass
47+
class str: pass # For convenience
48+
class object: pass
49+
class type: pass
50+
class ellipsis: pass
51+
class SupportsIndex(Protocol):
52+
def __index__(self) -> int: pass
53+
class list(Sequence[_T], Generic[_T]):
54+
@overload
55+
def __getitem__(self, i: int) -> _T: ...
56+
@overload
57+
def __getitem__(self, s: slice) -> list[_T]: ...
58+
def __contains__(self, item: object) -> bool: ...
59+
def __iter__(self) -> Iterator[_T]: ...

0 commit comments

Comments
 (0)