Skip to content

Commit 574744e

Browse files
elazargJukkaL
authored andcommitted
Cleanup check_reverse_op_method (#4017)
* Remove old comments, clean bail-out logic * Use fallback to find Instance when possible * Remove cast; move formatting logic to messages.py * Remove unused method * Rename variables to match check_overlapping_op_methods terminology * Remove many unused imports Potential fix to #3468, or at least avoids the crash (since it removes the unsafe cast).
1 parent 7264ae5 commit 574744e

File tree

6 files changed

+86
-59
lines changed

6 files changed

+86
-59
lines changed

mypy/checker.py

+48-48
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,34 @@
33
import itertools
44
import fnmatch
55
from contextlib import contextmanager
6-
import sys
76

87
from typing import (
98
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator
109
)
1110

1211
from mypy.errors import Errors, report_internal_error
1312
from mypy.nodes import (
14-
SymbolTable, Statement, MypyFile, Var, Expression, Lvalue,
13+
SymbolTable, Statement, MypyFile, Var, Expression, Lvalue, Node,
1514
OverloadedFuncDef, FuncDef, FuncItem, FuncBase, TypeInfo,
16-
ClassDef, GDEF, Block, AssignmentStmt, NameExpr, MemberExpr, IndexExpr,
15+
ClassDef, Block, AssignmentStmt, NameExpr, MemberExpr, IndexExpr,
1716
TupleExpr, ListExpr, ExpressionStmt, ReturnStmt, IfStmt,
1817
WhileStmt, OperatorAssignmentStmt, WithStmt, AssertStmt,
1918
RaiseStmt, TryStmt, ForStmt, DelStmt, CallExpr, IntExpr, StrExpr,
20-
BytesExpr, UnicodeExpr, FloatExpr, OpExpr, UnaryExpr, CastExpr, RevealTypeExpr, SuperExpr,
21-
TypeApplication, DictExpr, SliceExpr, LambdaExpr, TempNode, SymbolTableNode,
22-
Context, ListComprehension, ConditionalExpr, GeneratorExpr,
23-
Decorator, SetExpr, TypeVarExpr, NewTypeExpr, PrintStmt,
24-
LITERAL_TYPE, BreakStmt, PassStmt, ContinueStmt, ComparisonExpr, StarExpr,
25-
YieldFromExpr, NamedTupleExpr, TypedDictExpr, SetComprehension,
26-
DictionaryComprehension, ComplexExpr, EllipsisExpr, TypeAliasExpr,
27-
RefExpr, YieldExpr, BackquoteExpr, Import, ImportFrom, ImportAll, ImportBase,
28-
AwaitExpr, PromoteExpr, Node, EnumCallExpr,
29-
ARG_POS, MDEF,
30-
CONTRAVARIANT, COVARIANT, INVARIANT)
19+
UnicodeExpr, OpExpr, UnaryExpr, LambdaExpr, TempNode, SymbolTableNode,
20+
Context, Decorator, PrintStmt, BreakStmt, PassStmt, ContinueStmt,
21+
ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr,
22+
Import, ImportFrom, ImportAll, ImportBase,
23+
ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF,
24+
CONTRAVARIANT, COVARIANT, INVARIANT,
25+
)
3126
from mypy import nodes
3227
from mypy.literals import literal, literal_hash
3328
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any
3429
from mypy.types import (
3530
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
3631
Instance, NoneTyp, strip_type, TypeType, TypeOfAny,
3732
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
38-
true_only, false_only, function_type, is_named_instance, union_items
33+
true_only, false_only, function_type, is_named_instance, union_items,
3934
)
4035
from mypy.sametypes import is_same_type, is_same_types
4136
from mypy.messages import MessageBuilder, make_inferred_type_note
@@ -875,9 +870,11 @@ def is_trivial_body(self, block: Block) -> bool:
875870
(isinstance(stmt, ExpressionStmt) and
876871
isinstance(stmt.expr, EllipsisExpr)))
877872

878-
def check_reverse_op_method(self, defn: FuncItem, typ: CallableType,
879-
method: str, context: Context) -> None:
873+
def check_reverse_op_method(self, defn: FuncItem,
874+
reverse_type: CallableType, reverse_name: str,
875+
context: Context) -> None:
880876
"""Check a reverse operator method such as __radd__."""
877+
# Decides whether it's worth calling check_overlapping_op_methods().
881878

882879
# This used to check for some very obscure scenario. It now
883880
# just decides whether it's worth calling
@@ -890,54 +887,57 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType,
890887
[None, None],
891888
AnyType(TypeOfAny.special_form),
892889
self.named_type('builtins.function'))
893-
if not is_subtype(typ, method_type):
894-
self.msg.invalid_signature(typ, context)
890+
if not is_subtype(reverse_type, method_type):
891+
self.msg.invalid_signature(reverse_type, context)
895892
return
896893

897-
if method in ('__eq__', '__ne__'):
894+
if reverse_name in ('__eq__', '__ne__'):
898895
# These are defined for all objects => can't cause trouble.
899896
return
900897

901898
# With 'Any' or 'object' return type we are happy, since any possible
902899
# return value is valid.
903-
ret_type = typ.ret_type
900+
ret_type = reverse_type.ret_type
904901
if isinstance(ret_type, AnyType):
905902
return
906903
if isinstance(ret_type, Instance):
907904
if ret_type.type.fullname() == 'builtins.object':
908905
return
909-
910-
if len(typ.arg_types) == 2:
911-
# TODO check self argument kind
912-
913-
# Check for the issue described above.
914-
arg_type = typ.arg_types[1]
915-
other_method = nodes.normal_from_reverse_op[method]
916-
if isinstance(arg_type, Instance):
917-
if not arg_type.type.has_readable_member(other_method):
918-
return
919-
elif isinstance(arg_type, AnyType):
920-
return
921-
elif isinstance(arg_type, UnionType):
922-
if not arg_type.has_readable_member(other_method):
923-
return
924-
else:
925-
return
926-
927-
typ2 = self.expr_checker.analyze_external_member_access(
928-
other_method, arg_type, defn)
929-
self.check_overlapping_op_methods(
930-
typ, method, defn.info,
931-
typ2, other_method, cast(Instance, arg_type),
932-
defn)
906+
if reverse_type.arg_kinds[0] == ARG_STAR:
907+
reverse_type = reverse_type.copy_modified(arg_types=[reverse_type.arg_types[0]] * 2,
908+
arg_kinds=[ARG_POS] * 2,
909+
arg_names=[reverse_type.arg_names[0], "_"])
910+
assert len(reverse_type.arg_types) == 2
911+
912+
forward_name = nodes.normal_from_reverse_op[reverse_name]
913+
forward_inst = reverse_type.arg_types[1]
914+
if isinstance(forward_inst, TypeVarType):
915+
forward_inst = forward_inst.upper_bound
916+
if isinstance(forward_inst, (FunctionLike, TupleType, TypedDictType)):
917+
forward_inst = forward_inst.fallback
918+
if isinstance(forward_inst, TypeType):
919+
item = forward_inst.item
920+
if isinstance(item, Instance):
921+
opt_meta = item.type.metaclass_type
922+
if opt_meta is not None:
923+
forward_inst = opt_meta
924+
if not (isinstance(forward_inst, (Instance, UnionType))
925+
and forward_inst.has_readable_member(forward_name)):
926+
return
927+
forward_base = reverse_type.arg_types[1]
928+
forward_type = self.expr_checker.analyze_external_member_access(forward_name, forward_base,
929+
context=defn)
930+
self.check_overlapping_op_methods(reverse_type, reverse_name, defn.info,
931+
forward_type, forward_name, forward_base,
932+
context=defn)
933933

934934
def check_overlapping_op_methods(self,
935935
reverse_type: CallableType,
936936
reverse_name: str,
937937
reverse_class: TypeInfo,
938938
forward_type: Type,
939939
forward_name: str,
940-
forward_base: Instance,
940+
forward_base: Type,
941941
context: Context) -> None:
942942
"""Check for overlapping method and reverse method signatures.
943943
@@ -998,8 +998,8 @@ def check_overlapping_op_methods(self,
998998
if is_unsafe_overlapping_signatures(forward_tweaked,
999999
reverse_tweaked):
10001000
self.msg.operator_method_signatures_overlap(
1001-
reverse_class.name(), reverse_name,
1002-
forward_base.type.name(), forward_name, context)
1001+
reverse_class, reverse_name,
1002+
forward_base, forward_name, context)
10031003
elif isinstance(forward_item, Overloaded):
10041004
for item in forward_item.items():
10051005
self.check_overlapping_op_methods(

mypy/messages.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -932,12 +932,12 @@ def overloaded_signatures_ret_specific(self, index1: int, context: Context) -> N
932932
'of signature {}'.format(index1), context)
933933

934934
def operator_method_signatures_overlap(
935-
self, reverse_class: str, reverse_method: str, forward_class: str,
935+
self, reverse_class: TypeInfo, reverse_method: str, forward_class: Type,
936936
forward_method: str, context: Context) -> None:
937-
self.fail('Signatures of "{}" of "{}" and "{}" of "{}" '
937+
self.fail('Signatures of "{}" of "{}" and "{}" of {} '
938938
'are unsafely overlapping'.format(
939-
reverse_method, reverse_class,
940-
forward_method, forward_class),
939+
reverse_method, reverse_class.name(),
940+
forward_method, self.format(forward_class)),
941941
context)
942942

943943
def forward_operator_not_callable(

mypy/nodes.py

-6
Original file line numberDiff line numberDiff line change
@@ -2090,15 +2090,9 @@ def __getitem__(self, name: str) -> 'SymbolTableNode':
20902090
def __repr__(self) -> str:
20912091
return '<TypeInfo %s>' % self.fullname()
20922092

2093-
# IDEA: Refactor the has* methods to be more consistent and document
2094-
# them.
2095-
20962093
def has_readable_member(self, name: str) -> bool:
20972094
return self.get(name) is not None
20982095

2099-
def has_method(self, name: str) -> bool:
2100-
return self.get_method(name) is not None
2101-
21022096
def get_method(self, name: str) -> Optional[FuncBase]:
21032097
if self.mro is None: # Might be because of a previous error.
21042098
return None

mypy/types.py

+3
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,9 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance':
517517
def copy_modified(self, *, args: List[Type]) -> 'Instance':
518518
return Instance(self.type, args, self.line, self.column, self.erased)
519519

520+
def has_readable_member(self, name: str) -> bool:
521+
return self.type.has_readable_member(name)
522+
520523

521524
class TypeVarType(Type):
522525
"""A type variable type.

test-data/unit/check-classes.test

+23-1
Original file line numberDiff line numberDiff line change
@@ -1705,6 +1705,28 @@ class A:
17051705
def __add__(self, x: str) -> int: pass
17061706
def __radd__(self, x: 'A') -> str: pass
17071707

1708+
[case testReverseOperatorStar]
1709+
class B:
1710+
def __radd__(*self) -> int: pass
1711+
def __rsub__(*self: 'B') -> int: pass
1712+
1713+
[case testReverseOperatorTypeVar]
1714+
from typing import TypeVar
1715+
T = TypeVar("T", bound='Real')
1716+
class Real:
1717+
def __add__(self, other) -> str: ...
1718+
class Fraction(Real):
1719+
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping
1720+
1721+
[case testReverseOperatorTypeType]
1722+
from typing import TypeVar, Type
1723+
class Real(type):
1724+
def __add__(self, other) -> str: ...
1725+
class Fraction(Real):
1726+
def __radd__(self, other: Type['A']) -> Real: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "Type[A]" are unsafely overlapping
1727+
1728+
class A(metaclass=Real): pass
1729+
17081730
[case testAbstractReverseOperatorMethod]
17091731
import typing
17101732
from abc import abstractmethod
@@ -1751,7 +1773,7 @@ tmp/foo.pyi:6: error: Signatures of "__radd__" of "B" and "__add__" of "X" are u
17511773
[case testUnsafeOverlappingWithLineNo]
17521774
from typing import TypeVar
17531775
class Real:
1754-
def __add__(self, other): ...
1776+
def __add__(self, other) -> str: ...
17551777
class Fraction(Real):
17561778
def __radd__(self, other: Real) -> Real: ...
17571779
[out]

test-data/unit/check-namedtuple.test

+8
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,14 @@ my_eval(A([B(1), B(2)])) # OK
629629
[builtins fixtures/isinstancelist.pyi]
630630
[out]
631631

632+
[case testUnsafeOverlappingNamedTuple]
633+
from typing import NamedTuple
634+
635+
class Real(NamedTuple):
636+
def __sub__(self, other) -> str: return ""
637+
class Fraction(Real):
638+
def __rsub__(self, other: Real) -> Real: return other # E: Signatures of "__rsub__" of "Fraction" and "__sub__" of "Real" are unsafely overlapping
639+
632640
[case testForwardReferenceInNamedTuple]
633641
from typing import NamedTuple
634642

0 commit comments

Comments
 (0)