Skip to content

Add partial overload checks #5476

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

Merged
merged 13 commits into from
Aug 27, 2018
Merged
Next Next commit
Add support for partially overlapping types
This pull request adds more robust support for detecting partially
overlapping types. Specifically, it detects overlaps with...

1. TypedDicts
2. Tuples
3. Unions
4. TypeVars
5. Generic types containing variations of the above.

It also swaps out the code for detecting overlaps with operators and
removes some associated (and now unused) code.

This pull request builds on top of #5474
and #5475 -- once those two PRs are
merged, I'll rebase this diff if necessary.

This pull request also supercedes #5475 --
that PR contains basically the same code as these three PRs, just
smushed together.
  • Loading branch information
Michael0x2a committed Aug 16, 2018
commit af2109c4d23077f18847296ce24558b3d2125703
278 changes: 160 additions & 118 deletions mypy/checker.py

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,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 Down
52 changes: 50 additions & 2 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1955,13 +1955,61 @@ 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: Fraction) -> str: ...
class Fraction(Real):
def __radd__(self, other: T) -> T: ... # TODO: This should be unsafely overlapping
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 evaluating 'Fraction + Fraction': must define __add__

[case testReverseOperatorTypeType]
from typing import TypeVar, Type
Expand Down
78 changes: 78 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -2005,3 +2005,81 @@ def f(x: Union[A, str]) -> None:
if isinstance(x, A):
x.method_only_in_a()
[builtins fixtures/isinstance.pyi]

[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesNoStrictOptional]
# flags: --strict-optional
from typing import Optional, Union

class A: pass

def foo1(x: Union[A, str, None]) -> None:
if x is None:
reveal_type(x) # E: Revealed type is 'None'
elif isinstance(x, A):
reveal_type(x) # E: Revealed type is '__main__.A'
else:
reveal_type(x) # E: Revealed type is 'builtins.str'

def foo2(x: Optional[str]) -> None:
if x is None:
reveal_type(x) # E: Revealed type is 'None'
elif isinstance(x, A):
reveal_type(x)
else:
reveal_type(x) # E: Revealed type is 'builtins.str'
[builtins fixtures/isinstance.pyi]

[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesInNoStrictOptional]
# flags: --no-strict-optional
from typing import Optional, Union

class A: pass

def foo1(x: Union[A, str, None]) -> None:
if x is None:
# Since None is a subtype of all types in no-strict-optional,
# we can't really narrow the type here
reveal_type(x) # E: Revealed type is 'Union[__main__.A, builtins.str, None]'
elif isinstance(x, A):
# Note that Union[None, A] == A in no-strict-optional
reveal_type(x) # E: Revealed type is '__main__.A'
else:
reveal_type(x) # E: Revealed type is 'builtins.str'

def foo2(x: Optional[str]) -> None:
if x is None:
reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]'
elif isinstance(x, A):
# Mypy should, however, be able to skip impossible cases
reveal_type(x)
else:
reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]'
[builtins fixtures/isinstance.pyi]

[case testNoneCheckDoesNotNarrowWhenUsingTypeVars]
# flags: --strict-optional
from typing import TypeVar

T = TypeVar('T')

def foo(x: T) -> T:
out = None
out = x
if out is None:
pass
return out
[builtins fixtures/isinstance.pyi]

[case testNoneCheckDoesNotNarrowWhenUsingTypeVarsNoStrictOptional]
# flags: --no-strict-optional
from typing import TypeVar

T = TypeVar('T')

def foo(x: T) -> T:
out = None
out = x
if out is None:
pass
return out
[builtins fixtures/isinstance.pyi]
Copy link
Member

Choose a reason for hiding this comment

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

TBH I am not sure what these two tests test. Maybe add a note or somehow modify them so that it is more clear what is a potential failure?

Loading