Skip to content
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
37 changes: 29 additions & 8 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4294,6 +4294,22 @@ def check_op_reversible(

left_op = self.lookup_operator(op_name, left_type, context)
right_op = self.lookup_operator(rev_op_name, right_type, context)
left_runtime_type = (
left_type.fallback
if isinstance(left_type, FunctionLike) and left_type.is_type_obj()
else left_type
)
right_runtime_type = (
right_type.fallback
if isinstance(right_type, FunctionLike) and right_type.is_type_obj()
else right_type
)
if isinstance(left_type, TypeType):
if info := type_info_from_type(left_type):
left_runtime_type = info.metaclass_type or self.named_type("builtins.type")
if isinstance(right_type, TypeType):
if info := type_info_from_type(right_type):
right_runtime_type = info.metaclass_type or self.named_type("builtins.type")

# STEP 2a:
# We figure out in which order Python will call the operator methods. As it
Expand All @@ -4303,7 +4319,9 @@ def check_op_reversible(
# We store the determined order inside the 'variants_raw' variable,
# which records tuples containing the method, base type, and the argument.

if op_name in operators.op_methods_that_shortcut and is_same_type(left_type, right_type):
if op_name in operators.op_methods_that_shortcut and is_same_type(
left_runtime_type, right_runtime_type
):
# When we do "A() + A()", for example, Python will only call the __add__ method,
# never the __radd__ method.
#
Expand All @@ -4314,20 +4332,23 @@ def check_op_reversible(
elif (
(
# Checking (A implies B) using the logically equivalent (not A or B), where
# A: left and right are both `Instance` objects
# A: left and right have `Instance` runtime types
# B: right's __rop__ method is different from left's __op__ method
not (isinstance(left_type, Instance) and isinstance(right_type, Instance))
not (
isinstance(left_runtime_type, Instance)
and isinstance(right_runtime_type, Instance)
)
or (
self.lookup_definer(left_type, op_name)
!= self.lookup_definer(right_type, rev_op_name)
self.lookup_definer(left_runtime_type, op_name)
!= self.lookup_definer(right_runtime_type, rev_op_name)
and (
left_type.type.alt_promote is None
or left_type.type.alt_promote.type is not right_type.type
left_runtime_type.type.alt_promote is None
or left_runtime_type.type.alt_promote.type is not right_runtime_type.type
)
)
)
# Note: use `covers_at_runtime` instead of `is_subtype` (#19006)
and covers_at_runtime(right_type, left_type)
and covers_at_runtime(right_runtime_type, left_runtime_type)
):
# 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
41 changes: 41 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2582,6 +2582,47 @@ class C(B): pass
# A refinement made by a parent also counts
reveal_type(A() + C()) # N: Revealed type is "builtins.str"

[case testReverseOperatorOrderingForClassObjects]
from typing import TypeVar, assert_type

class LeftMeta(type):
def __mul__(cls, other: object) -> int: return 1
def __rmul__(cls, other: object) -> str: return "left"

class RightMeta(LeftMeta):
def __rmul__(cls, other: object) -> str: return "right"

class InheritedMeta(LeftMeta): pass

class OtherMeta(type):
def __rmul__(cls, other: object) -> str: return "other"

class A(metaclass=LeftMeta): pass
class B(metaclass=RightMeta): pass
class C(metaclass=InheritedMeta): pass
class D(metaclass=OtherMeta): pass
class OtherD(metaclass=OtherMeta): pass
class SubA(A, metaclass=RightMeta): pass

assert_type(A * B, str)
assert_type(A * C, int)
assert_type(A * D, int)
assert_type(SubA * B, int)
D * OtherD # E: Unsupported left operand type for * ("type[D]")

def check_parameters(a: type[A], b: type[B], c: type[C], d: type[D], sub: type[SubA]) -> None:
assert_type(a * b, str)
assert_type(A * b, str)
assert_type(a * B, str)
assert_type(a * c, int)
assert_type(a * d, int)
assert_type(sub * b, int)

T = TypeVar("T", bound=A)

def check_bound(a: type[T], b: type[B]) -> None:
assert_type(a * b, str)

[case testReverseOperatorWithOverloads1]
from typing import overload

Expand Down
Loading