Skip to content

[OLD] Add more robust support for detecting partially overlapping types #5280

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fae9016
Add more robust support for detecting partially overlapping types
Michael0x2a Jun 4, 2018
d91b0d6
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Jul 6, 2018
1f17aff
WIP commit
Michael0x2a Jul 6, 2018
41cda3e
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Jul 17, 2018
4cdeab7
Refactor and add some comments
Michael0x2a Jul 17, 2018
813ddf7
Add distutils and encodings as stdlib modules (#5372)
ysangkok Jul 18, 2018
9bb0477
Sync typeshed (#5373)
Michael0x2a Jul 18, 2018
2e4ab7c
Enable --no-silence-site-packages in eval tests (#5370)
Michael0x2a Jul 18, 2018
ee86385
WIP: Try switching to using relevant_items()
Michael0x2a Jul 18, 2018
4cad672
WIP: try modifying interaction with overloads, classes, and invariance
Michael0x2a Jul 18, 2018
b554023
WIP: Allow overlapping type checks to disable promotions
Michael0x2a Jul 20, 2018
4059f60
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Jul 20, 2018
18305f2
WIP: Make checks less sensitive to 'Any' weirdness
Michael0x2a Jul 20, 2018
bb6fbc5
WIP: Refine and clean up overlapping meets logic
Michael0x2a Jul 20, 2018
55a01cf
Add unit tests to cover a failure fixed by previous commit
Michael0x2a Jul 20, 2018
022e75b
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Jul 20, 2018
d559f43
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Aug 5, 2018
7e2eb9b
WIP: refactor operator code
Michael0x2a Aug 8, 2018
f098191
Fix typo in test
Michael0x2a Aug 8, 2018
b57c39a
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Aug 8, 2018
539ed6a
WIP: Try adding more precise support for reversable operators
Michael0x2a Aug 11, 2018
b5e42d3
WIP: operators refinement (incomplete)
Michael0x2a Aug 13, 2018
7eb85dd
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Aug 13, 2018
e12fa0b
WIP: Modify Any fallback from reverse operators
Michael0x2a Aug 14, 2018
6702982
Merge branch 'master' into overloads-partially-overlapping-types
Michael0x2a Aug 14, 2018
e564df3
WIP: Fix last failing tests?
Michael0x2a Aug 14, 2018
4e3c8b8
Add missing return
Michael0x2a Aug 14, 2018
71437c4
WIP: Misc cleanup
Michael0x2a Aug 14, 2018
99d48b7
Misc cleanup
Michael0x2a Aug 14, 2018
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
406 changes: 239 additions & 167 deletions mypy/checker.py

Large diffs are not rendered by default.

313 changes: 223 additions & 90 deletions mypy/checkexpr.py

Large diffs are not rendered by default.

359 changes: 230 additions & 129 deletions mypy/meet.py

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,12 @@ def overloaded_signatures_overlap(self, index1: int, index2: int, context: Conte
self.fail('Overloaded function signatures {} and {} overlap with '
'incompatible return types'.format(index1, index2), context)

def overloaded_signatures_partial_overlap(self, index1: int, index2: int,
context: Context) -> None:
self.fail('Overloaded function signatures {} and {} '.format(index1, index2)
+ 'are partially overlapping: the two signatures may return '
+ 'incompatible types given certain calls', context)

def overloaded_signature_will_never_match(self, index1: int, index2: int,
context: Context) -> None:
self.fail(
Expand All @@ -994,6 +1000,22 @@ def overloaded_signatures_ret_specific(self, index: int, context: Context) -> No
self.fail('Overloaded function implementation cannot produce return type '
'of signature {}'.format(index), context)

def reverse_operator_method_never_called(self,
op: str,
forward_method: str,
reverse_type: Type,
reverse_method: str,
context: Context) -> None:
msg = "{rfunc} will not be called when running '{cls} {op} {cls}': must define {ffunc}"
self.note(
msg.format(
op=op,
ffunc=forward_method,
rfunc=reverse_method,
cls=self.format_bare(reverse_type),
),
context=context)

def operator_method_signatures_overlap(
self, reverse_class: TypeInfo, reverse_method: str, forward_class: Type,
forward_method: str, context: Context) -> None:
Expand Down
22 changes: 22 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,8 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
'in': '__contains__',
} # type: Dict[str, str]

op_methods_to_symbols = {v: k for (k, v) in op_methods.items()}

comparison_fallback_method = '__cmp__'
ops_falling_back_to_cmp = {'__ne__', '__eq__',
'__lt__', '__le__',
Expand Down Expand Up @@ -1506,6 +1508,26 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
'__le__': '__ge__',
}

# Suppose we have some class A. When we do A() + A(), Python will only check
# the output of A().__add__(A()) and skip calling the __radd__ method entirely.
# This shortcut is used only for the following methods:
op_methods_that_shortcut = {
'__add__',
'__sub__',
'__mul__',
'__truediv__',
'__mod__',
'__divmod__',
'__floordiv__',
'__pow__',
'__matmul__',
'__and__',
'__or__',
'__xor__',
'__lshift__',
'__rshift__',
}

normal_from_reverse_op = dict((m, n) for n, m in reverse_op_methods.items())
reverse_op_method_set = set(reverse_op_methods.values())

Expand Down
3 changes: 2 additions & 1 deletion mypy/sametypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def visit_callable_type(self, left: CallableType) -> bool:

def visit_tuple_type(self, left: TupleType) -> bool:
if isinstance(self.right, TupleType):
return is_same_types(left.items, self.right.items)
return (is_same_type(left.fallback, self.right.fallback)
and is_same_types(left.items, self.right.items))
else:
return False

Expand Down
201 changes: 127 additions & 74 deletions mypy/subtypes.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,9 @@ def deserialize(cls, data: JsonDict) -> 'TypedDictType':
set(data['required_keys']),
Instance.deserialize(data['fallback']))

def has_optional_keys(self) -> bool:
return any(key not in self.required_keys for key in self.items)

def is_anonymous(self) -> bool:
return self.fallback.type.fullname() == 'typing.Mapping'

Expand Down
46 changes: 24 additions & 22 deletions mypy/typestate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
and potentially other mutable TypeInfo state. This module contains mutable global state.
"""

from typing import Dict, Set, Tuple, Optional
from typing import Any, Dict, Set, Tuple, Optional

MYPY = False
if MYPY:
Expand All @@ -12,6 +12,17 @@
from mypy.types import Instance
from mypy.server.trigger import make_trigger

# Represents that the 'left' instance is a subtype of the 'right' instance
SubtypeRelationship = Tuple[Instance, Instance]

# A hash encoding the specific conditions under which we performed the subtype check.
# (e.g. did we want a proper subtype? A regular subtype while ignoring variance?)
SubtypeKind = int

# A cache that keeps track of whether the given TypeInfo is a part of a particular
# subtype relationship
SubtypeCache = Dict[TypeInfo, Dict[SubtypeKind, Set[SubtypeRelationship]]]


class TypeState:
"""This class provides subtype caching to improve performance of subtype checks.
Expand All @@ -23,13 +34,11 @@ class TypeState:
The protocol dependencies however are only stored here, and shouldn't be deleted unless
not needed any more (e.g. during daemon shutdown).
"""
# 'caches' and 'caches_proper' are subtype caches, implemented as sets of pairs
# of (subtype, supertype), where supertypes are instances of given TypeInfo.
# '_subtype_caches' keeps track of (subtype, supertype) pairs where supertypes are
# instances of the given TypeInfo. The cache also keeps track of the specific
# *kind* of subtyping relationship, which we represent as an arbitrary hashable tuple.
# We need the caches, since subtype checks for structural types are very slow.
# _subtype_caches_proper is for caching proper subtype checks (i.e. not assuming that
# Any is consistent with every type).
_subtype_caches = {} # type: ClassVar[Dict[TypeInfo, Set[Tuple[Instance, Instance]]]]
_subtype_caches_proper = {} # type: ClassVar[Dict[TypeInfo, Set[Tuple[Instance, Instance]]]]
_subtype_caches = {} # type: ClassVar[SubtypeCache]

# This contains protocol dependencies generated after running a full build,
# or after an update. These dependencies are special because:
Expand Down Expand Up @@ -70,13 +79,11 @@ class TypeState:
def reset_all_subtype_caches(cls) -> None:
"""Completely reset all known subtype caches."""
cls._subtype_caches = {}
cls._subtype_caches_proper = {}

@classmethod
def reset_subtype_caches_for(cls, info: TypeInfo) -> None:
"""Reset subtype caches (if any) for a given supertype TypeInfo."""
cls._subtype_caches.setdefault(info, set()).clear()
cls._subtype_caches_proper.setdefault(info, set()).clear()
cls._subtype_caches.setdefault(info, dict()).clear()

@classmethod
def reset_all_subtype_caches_for(cls, info: TypeInfo) -> None:
Expand All @@ -85,20 +92,15 @@ def reset_all_subtype_caches_for(cls, info: TypeInfo) -> None:
cls.reset_subtype_caches_for(item)

@classmethod
def is_cached_subtype_check(cls, left: Instance, right: Instance) -> bool:
return (left, right) in cls._subtype_caches.setdefault(right.type, set())

@classmethod
def is_cached_proper_subtype_check(cls, left: Instance, right: Instance) -> bool:
return (left, right) in cls._subtype_caches_proper.setdefault(right.type, set())

@classmethod
def record_subtype_cache_entry(cls, left: Instance, right: Instance) -> None:
cls._subtype_caches.setdefault(right.type, set()).add((left, right))
def is_cached_subtype_check(cls, kind: SubtypeKind, left: Instance, right: Instance) -> bool:
subtype_kinds = cls._subtype_caches.setdefault(right.type, dict())
return (left, right) in subtype_kinds.setdefault(kind, set())

@classmethod
def record_proper_subtype_cache_entry(cls, left: Instance, right: Instance) -> None:
cls._subtype_caches_proper.setdefault(right.type, set()).add((left, right))
def record_subtype_cache_entry(cls, kind: SubtypeKind,
left: Instance, right: Instance) -> None:
subtype_kinds = cls._subtype_caches.setdefault(right.type, dict())
subtype_kinds.setdefault(kind, set()).add((left, right))

@classmethod
def reset_protocol_deps(cls) -> None:
Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -661,16 +661,16 @@ reveal_type(D.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1)

A() < A()
B() < B()
A() < B() # E: Unsupported operand types for > ("B" and "A")
A() < B() # E: Unsupported operand types for < ("A" and "B")

C() > A()
C() > B()
C() > C()
C() > D() # E: Unsupported operand types for < ("D" and "C")
C() > D() # E: Unsupported operand types for > ("C" and "D")

D() >= A()
D() >= B() # E: Unsupported operand types for <= ("B" and "D")
D() >= C() # E: Unsupported operand types for <= ("C" and "D")
D() >= B() # E: Unsupported operand types for >= ("D" and "B")
D() >= C() # E: Unsupported operand types for >= ("D" and "C")
D() >= D()

A() <= 1 # E: Unsupported operand types for <= ("A" and "int")
Expand Down
84 changes: 80 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,33 @@ class A:
class B(A):
def __add__(self, x): pass

[case testOperatorMethodAgainstSameType]
class A:
def __add__(self, x: int) -> 'A':
if isinstance(x, int):
return A()
else:
return NotImplemented

def __radd__(self, x: 'A') -> 'A':
if isinstance(x, A):
return A()
else:
return NotImplemented

class B(A): pass

# Note: This is a runtime error. If we run x.__add__(y)
# where x and y are *not* the same type, Python will not try
# calling __radd__.
A() + A() # E: Unsupported operand types for + ("A" and "A") \
# N: __radd__ will not be called when running 'A + A': must define __add__

# Here, Python *will* call __radd__(...)
reveal_type(B() + A()) # E: Revealed type is '__main__.A'
reveal_type(A() + B()) # E: Revealed type is '__main__.A'
[builtins fixtures/isinstance.pyi]

[case testOperatorMethodOverrideWithIdenticalOverloadedType]
from foo import *
[file foo.pyi]
Expand Down Expand Up @@ -1755,20 +1782,69 @@ class B:
def __radd__(*self) -> int: pass
def __rsub__(*self: 'B') -> int: pass

[case testReverseOperatorTypeVar]
[case testReverseOperatorTypeVar1]
from typing import TypeVar, Any
T = TypeVar("T", bound='Real')
class Real:
def __add__(self, other: Any) -> str: ...
class Fraction(Real):
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping

# Note: When doing A + B and if B is a subtype of A, we will always call B.__radd__(A) first
# and only try A.__add__(B) second if necessary.
reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'

# Note: When doing A + A, we only ever call A.__add__(A), never A.__radd__(A).
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'

[case testReverseOperatorTypeVar2a]
from typing import TypeVar
T = TypeVar("T", bound='Real')
class Real:
def __add__(self, other) -> str: ...
def __add__(self, other: Fraction) -> str: ...
class Fraction(Real):
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping

reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'


[case testReverseOperatorTypeVar2b]
from typing import TypeVar
T = TypeVar("T", Real, Fraction)
class Real:
def __add__(self, other: Fraction) -> str: ...
class Fraction(Real):
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "Real" are unsafely overlapping

reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'

[case testReverseOperatorTypeVar3]
from typing import TypeVar, Any
T = TypeVar("T", bound='Real')
class Real:
def __add__(self, other: FractionChild) -> str: ...
class Fraction(Real):
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping
class FractionChild(Fraction): pass

reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
reveal_type(FractionChild() + Fraction()) # E: Revealed type is '__main__.FractionChild*'
reveal_type(FractionChild() + FractionChild()) # E: Revealed type is 'builtins.str'

# Runtime error: we try calling __add__, it doesn't match, and we don't try __radd__ since
# the LHS and the RHS are not the same.
Fraction() + Fraction() # E: Unsupported operand types for + ("Fraction" and "Fraction") \
# N: __radd__ will not be called when running 'Fraction + Fraction': must define __add__

[case testReverseOperatorTypeType]
from typing import TypeVar, Type
class Real(type):
def __add__(self, other) -> str: ...
def __add__(self, other: FractionChild) -> str: ...
class Fraction(Real):
def __radd__(self, other: Type['A']) -> Real: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "Type[A]" are unsafely overlapping
class FractionChild(Fraction): pass

class A(metaclass=Real): pass

Expand Down Expand Up @@ -1811,7 +1887,7 @@ class B:
@overload
def __radd__(self, x: A) -> str: pass # Error
class X:
def __add__(self, x): pass
def __add__(self, x: B) -> int: pass
[out]
tmp/foo.pyi:6: error: Signatures of "__radd__" of "B" and "__add__" of "X" are unsafely overlapping

Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ class B:
def __gt__(self, o: 'B') -> bool: pass
[builtins fixtures/bool.pyi]
[out]
main:3: error: Unsupported operand types for > ("A" and "A")
main:5: error: Unsupported operand types for > ("A" and "A")
main:3: error: Unsupported operand types for < ("A" and "A")
main:5: error: Unsupported operand types for < ("A" and "A")
main:5: error: Unsupported operand types for > ("A" and "A")


[case testChainedCompBoolRes]
Expand Down Expand Up @@ -664,7 +664,7 @@ A() + cast(Any, 1)
class C:
def __gt__(self, x: 'A') -> object: pass
class A:
def __lt__(self, x: C) -> int: pass
def __lt__(self, x: C) -> int: pass # E: Signatures of "__lt__" of "A" and "__gt__" of "C" are unsafely overlapping
class B:
def __gt__(self, x: A) -> str: pass
s = None # type: str
Expand Down
Loading