Skip to content

Commit 6b4e7a1

Browse files
committed
Move this check to special case in check_op
1 parent 80b9fcd commit 6b4e7a1

File tree

3 files changed

+62
-5
lines changed

3 files changed

+62
-5
lines changed

mypy/checkexpr.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4038,7 +4038,21 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
40384038
# We store the determined order inside the 'variants_raw' variable,
40394039
# which records tuples containing the method, base type, and the argument.
40404040

4041-
if op_name in operators.op_methods_that_shortcut and is_same_type(left_type, right_type):
4041+
if (
4042+
op_name in operators.op_methods_that_shortcut
4043+
and is_same_type(left_type, right_type)
4044+
and not (
4045+
# We consider typevars with equal IDs "same types" even if some narrowing
4046+
# has been applied. However, different bounds here might come from union
4047+
# expansion applied earlier, so we are not supposed to check them as
4048+
# being same types here. For plain union items `is_same_type` will
4049+
# return false, but not for typevars having these items as bounds.
4050+
# See testReversibleOpOnTypeVarProtocol.
4051+
isinstance(left_type, TypeVarType)
4052+
and isinstance(right_type, TypeVarType)
4053+
and not is_same_type(left_type.upper_bound, right_type.upper_bound)
4054+
)
4055+
):
40424056
# When we do "A() + A()", for example, Python will only call the __add__ method,
40434057
# never the __radd__ method.
40444058
#

mypy/subtypes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ def is_same_type(
273273
):
274274
return all(is_same_type(x, y) for x, y in zip(a.args, b.args))
275275
elif isinstance(a, TypeVarType) and isinstance(b, TypeVarType) and a.id == b.id:
276+
# This is not only a performance optimization. Deeper check will compare upper
277+
# bounds, but we want to consider copies of the same type variable "same type".
278+
# This makes sense semantically: even we have narrowed the upper bound somehow,
279+
# it's still the same object it used to be before.
276280
return True
277281

278282
# Note that using ignore_promotions=True (default) makes types like int and int64

test-data/unit/check-expressions.test

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -734,17 +734,56 @@ def check(x: _T, y: _T) -> bool:
734734
[case testReversibleOpOnTypeVarProtocol]
735735
# https://github.com/python/mypy/issues/18203
736736
from typing import Protocol, TypeVar, Union
737-
from typing_extensions import Self
737+
from typing_extensions import Self, runtime_checkable
738738

739739
class A(Protocol):
740740
def __add__(self, other: Union[int, Self]) -> Self: ...
741741
def __radd__(self, other: Union[int, Self]) -> Self: ...
742742

743743
AT = TypeVar("AT", bound=Union[int, A])
744744

745-
def f(a: AT, _b: AT) -> None:
746-
a + a
747-
[builtins fixtures/ops.pyi]
745+
def f(a: AT, b: AT) -> None:
746+
reveal_type(a + a) # N: Revealed type is "Union[builtins.int, AT`-1]"
747+
reveal_type(a + b) # N: Revealed type is "Union[builtins.int, AT`-1]"
748+
if isinstance(a, int):
749+
reveal_type(a) # N: Revealed type is "AT`-1"
750+
reveal_type(a + a) # N: Revealed type is "builtins.int"
751+
reveal_type(a + b) # N: Revealed type is "Union[builtins.int, AT`-1]"
752+
reveal_type(b + a) # N: Revealed type is "Union[builtins.int, AT`-1]"
753+
754+
@runtime_checkable
755+
class B(Protocol):
756+
def __radd__(self, other: Union[int, Self]) -> Self: ...
757+
758+
BT = TypeVar("BT", bound=Union[int, B])
759+
760+
def g(a: BT, b: BT) -> None:
761+
reveal_type(a + a) # E: Unsupported left operand type for + ("BT") \
762+
# N: Both left and right operands are unions \
763+
# N: Revealed type is "Union[builtins.int, BT`-1, Any]"
764+
reveal_type(a + b) # E: Unsupported left operand type for + ("BT") \
765+
# N: Both left and right operands are unions \
766+
# N: Revealed type is "Union[builtins.int, BT`-1, Any]"
767+
if isinstance(a, int):
768+
reveal_type(a) # N: Revealed type is "BT`-1"
769+
reveal_type(0 + a) # N: Revealed type is "builtins.int"
770+
reveal_type(a + 0) # N: Revealed type is "builtins.int"
771+
reveal_type(a + a) # N: Revealed type is "builtins.int"
772+
reveal_type(a + b) # N: Revealed type is "Union[builtins.int, BT`-1]"
773+
reveal_type(b + a) # E: Unsupported left operand type for + ("BT") \
774+
# N: Left operand is of type "BT" \
775+
# N: Revealed type is "Union[builtins.int, Any]"
776+
if isinstance(a, B):
777+
reveal_type(a) # N: Revealed type is "BT`-1"
778+
reveal_type(0 + a) # N: Revealed type is "BT`-1"
779+
reveal_type(a + 0) # E: Unsupported left operand type for + ("BT") \
780+
# N: Revealed type is "Any"
781+
reveal_type(a + a) # E: Unsupported left operand type for + ("BT") \
782+
# N: Revealed type is "Any"
783+
reveal_type(a + b) # E: Unsupported left operand type for + ("BT") \
784+
# N: Right operand is of type "BT" \
785+
# N: Revealed type is "Any"
786+
[builtins fixtures/isinstance.pyi]
748787

749788

750789
[case testErrorContextAndBinaryOperators]

0 commit comments

Comments
 (0)