Skip to content

Patch check_op_reversible to support tuple subclasses. #19046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
from mypy.semanal_enum import ENUM_BASES
from mypy.state import state
from mypy.subtypes import (
covers_at_runtime,
find_member,
is_equivalent,
is_same_type,
Expand Down Expand Up @@ -4048,14 +4049,21 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:

variants_raw = [(op_name, left_op, left_type, right_expr)]
elif (
is_subtype(right_type, left_type)
and isinstance(left_type, Instance)
and isinstance(right_type, Instance)
and not (
left_type.type.alt_promote is not None
and left_type.type.alt_promote.type is right_type.type
# Note: use `covers_at_runtime` instead of `is_subtype` (#19006)
covers_at_runtime(right_type, left_type)
and (
# Checking (A implies B) using the logically equivalent (not A or B), where
Copy link
Contributor

@jorenham jorenham May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formally called "modus ponens" (https://wikipedia.org/wiki/Modus_ponens), and the binary operator itself is called the "material conditional" (https://wikipedia.org/wiki/Material_conditional) 🤓

# A: left and right are both `Instance` objects
# B: right's __rop__ method is different from left's __op__ method
not (isinstance(left_type, Instance) and isinstance(right_type, Instance))
or (
lookup_definer(left_type, op_name) != lookup_definer(right_type, rev_op_name)
and (
left_type.type.alt_promote is None
or left_type.type.alt_promote.type is not right_type.type
)
)
)
and lookup_definer(left_type, op_name) != lookup_definer(right_type, rev_op_name)
):
# When we do "A() + B()" where B is a subclass of A, we'll actually try calling
# B's __radd__ method first, but ONLY if B explicitly defines or overrides the
Expand Down
23 changes: 23 additions & 0 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,29 @@ class B:
s: str
s = A() + B() # E: Unsupported operand types for + ("A" and "B")


[case testReverseBinaryOperator4]
from typing import assert_type, Never

class Size(tuple[int, ...]):
def __add__(self, other: tuple[int, ...], /) -> "Size": return Size() # type: ignore[override]
def __radd__(self, other: tuple[int, ...], /) -> "Size": return Size()

size: Size = Size([3, 4])
tup0: tuple[()] = ()
tup1: tuple[int] = (1,)
tup2: tuple[int, int] = (1, 2)
tupN: tuple[int, ...] = (1, 2, 3)
tupX: tuple[Never, ...] = ()

assert_type(tup0 + size, Size)
assert_type(tup1 + size, Size)
assert_type(tup2 + size, Size)
assert_type(tupN + size, Size)
assert_type(tupX + size, Size)

[builtins fixtures/tuple-typeshed.pyi]

[case testBinaryOperatorWithAnyRightOperand]
from typing import Any, cast
class A: pass
Expand Down
59 changes: 59 additions & 0 deletions test-data/unit/fixtures/tuple-typeshed.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# tuple definition from typeshed,
from typing import (
Generic,
Sequence,
TypeVar,
Iterable,
Iterator,
Any,
overload,
Self,
Protocol,
)
from types import GenericAlias

_T = TypeVar("_T")
_T_co = TypeVar('_T_co', covariant=True)

class tuple(Sequence[_T_co], Generic[_T_co]):
def __new__(cls, iterable: Iterable[_T_co] = ..., /) -> Self: ...
def __len__(self) -> int: ...
def __contains__(self, key: object, /) -> bool: ...
@overload
def __getitem__(self, key: SupportsIndex, /) -> _T_co: ...
@overload
def __getitem__(self, key: slice, /) -> tuple[_T_co, ...]: ...
def __iter__(self) -> Iterator[_T_co]: ...
def __lt__(self, value: tuple[_T_co, ...], /) -> bool: ...
def __le__(self, value: tuple[_T_co, ...], /) -> bool: ...
def __gt__(self, value: tuple[_T_co, ...], /) -> bool: ...
def __ge__(self, value: tuple[_T_co, ...], /) -> bool: ...
def __eq__(self, value: object, /) -> bool: ...
def __hash__(self) -> int: ...
@overload
def __add__(self, value: tuple[_T_co, ...], /) -> tuple[_T_co, ...]: ...
@overload
def __add__(self, value: tuple[_T, ...], /) -> tuple[_T_co | _T, ...]: ...
def __mul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...
def __rmul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...
def count(self, value: Any, /) -> int: ...
def index(self, value: Any, start: SupportsIndex = ..., stop: SupportsIndex = ..., /) -> int: ...
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...

class dict: pass
class int: pass
class slice: pass
class bool(int): pass
class str: pass # For convenience
class object: pass
class type: pass
class ellipsis: pass
class SupportsIndex(Protocol):
def __index__(self) -> int: pass
class list(Sequence[_T], Generic[_T]):
@overload
def __getitem__(self, i: int) -> _T: ...
@overload
def __getitem__(self, s: slice) -> list[_T]: ...
def __contains__(self, item: object) -> bool: ...
def __iter__(self) -> Iterator[_T]: ...