Skip to content

Commit b9c8d22

Browse files
committed
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.
1 parent 0243661 commit b9c8d22

8 files changed

+1028
-269
lines changed

mypy/checker.py

+160-118
Large diffs are not rendered by default.

mypy/meet.py

+230-129
Large diffs are not rendered by default.

mypy/messages.py

+6
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,12 @@ def overloaded_signatures_overlap(self, index1: int, index2: int, context: Conte
973973
self.fail('Overloaded function signatures {} and {} overlap with '
974974
'incompatible return types'.format(index1, index2), context)
975975

976+
def overloaded_signatures_partial_overlap(self, index1: int, index2: int,
977+
context: Context) -> None:
978+
self.fail('Overloaded function signatures {} and {} '.format(index1, index2)
979+
+ 'are partially overlapping: the two signatures may return '
980+
+ 'incompatible types given certain calls', context)
981+
976982
def overloaded_signature_will_never_match(self, index1: int, index2: int,
977983
context: Context) -> None:
978984
self.fail(

test-data/unit/check-classes.test

+50-2
Original file line numberDiff line numberDiff line change
@@ -1853,13 +1853,61 @@ class B:
18531853
def __radd__(*self) -> int: pass
18541854
def __rsub__(*self: 'B') -> int: pass
18551855

1856-
[case testReverseOperatorTypeVar]
1856+
[case testReverseOperatorTypeVar1]
1857+
from typing import TypeVar, Any
1858+
T = TypeVar("T", bound='Real')
1859+
class Real:
1860+
def __add__(self, other: Any) -> str: ...
1861+
class Fraction(Real):
1862+
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping
1863+
1864+
# Note: When doing A + B and if B is a subtype of A, we will always call B.__radd__(A) first
1865+
# and only try A.__add__(B) second if necessary.
1866+
reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
1867+
1868+
# Note: When doing A + A, we only ever call A.__add__(A), never A.__radd__(A).
1869+
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'
1870+
1871+
[case testReverseOperatorTypeVar2a]
18571872
from typing import TypeVar
18581873
T = TypeVar("T", bound='Real')
18591874
class Real:
18601875
def __add__(self, other: Fraction) -> str: ...
18611876
class Fraction(Real):
1862-
def __radd__(self, other: T) -> T: ... # TODO: This should be unsafely overlapping
1877+
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping
1878+
1879+
reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
1880+
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'
1881+
1882+
1883+
[case testReverseOperatorTypeVar2b]
1884+
from typing import TypeVar
1885+
T = TypeVar("T", Real, Fraction)
1886+
class Real:
1887+
def __add__(self, other: Fraction) -> str: ...
1888+
class Fraction(Real):
1889+
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "Real" are unsafely overlapping
1890+
1891+
reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
1892+
reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str'
1893+
1894+
[case testReverseOperatorTypeVar3]
1895+
from typing import TypeVar, Any
1896+
T = TypeVar("T", bound='Real')
1897+
class Real:
1898+
def __add__(self, other: FractionChild) -> str: ...
1899+
class Fraction(Real):
1900+
def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping
1901+
class FractionChild(Fraction): pass
1902+
1903+
reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*'
1904+
reveal_type(FractionChild() + Fraction()) # E: Revealed type is '__main__.FractionChild*'
1905+
reveal_type(FractionChild() + FractionChild()) # E: Revealed type is 'builtins.str'
1906+
1907+
# Runtime error: we try calling __add__, it doesn't match, and we don't try __radd__ since
1908+
# the LHS and the RHS are not the same.
1909+
Fraction() + Fraction() # E: Unsupported operand types for + ("Fraction" and "Fraction") \
1910+
# N: __radd__ will not be called when running 'Fraction + Fraction': must define __add__
18631911

18641912
[case testReverseOperatorTypeType]
18651913
from typing import TypeVar, Type

test-data/unit/check-isinstance.test

+78
Original file line numberDiff line numberDiff line change
@@ -2005,3 +2005,81 @@ def f(x: Union[A, str]) -> None:
20052005
if isinstance(x, A):
20062006
x.method_only_in_a()
20072007
[builtins fixtures/isinstance.pyi]
2008+
2009+
[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesNoStrictOptional]
2010+
# flags: --strict-optional
2011+
from typing import Optional, Union
2012+
2013+
class A: pass
2014+
2015+
def foo1(x: Union[A, str, None]) -> None:
2016+
if x is None:
2017+
reveal_type(x) # E: Revealed type is 'None'
2018+
elif isinstance(x, A):
2019+
reveal_type(x) # E: Revealed type is '__main__.A'
2020+
else:
2021+
reveal_type(x) # E: Revealed type is 'builtins.str'
2022+
2023+
def foo2(x: Optional[str]) -> None:
2024+
if x is None:
2025+
reveal_type(x) # E: Revealed type is 'None'
2026+
elif isinstance(x, A):
2027+
reveal_type(x)
2028+
else:
2029+
reveal_type(x) # E: Revealed type is 'builtins.str'
2030+
[builtins fixtures/isinstance.pyi]
2031+
2032+
[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesInNoStrictOptional]
2033+
# flags: --no-strict-optional
2034+
from typing import Optional, Union
2035+
2036+
class A: pass
2037+
2038+
def foo1(x: Union[A, str, None]) -> None:
2039+
if x is None:
2040+
# Since None is a subtype of all types in no-strict-optional,
2041+
# we can't really narrow the type here
2042+
reveal_type(x) # E: Revealed type is 'Union[__main__.A, builtins.str, None]'
2043+
elif isinstance(x, A):
2044+
# Note that Union[None, A] == A in no-strict-optional
2045+
reveal_type(x) # E: Revealed type is '__main__.A'
2046+
else:
2047+
reveal_type(x) # E: Revealed type is 'builtins.str'
2048+
2049+
def foo2(x: Optional[str]) -> None:
2050+
if x is None:
2051+
reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]'
2052+
elif isinstance(x, A):
2053+
# Mypy should, however, be able to skip impossible cases
2054+
reveal_type(x)
2055+
else:
2056+
reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]'
2057+
[builtins fixtures/isinstance.pyi]
2058+
2059+
[case testNoneCheckDoesNotNarrowWhenUsingTypeVars]
2060+
# flags: --strict-optional
2061+
from typing import TypeVar
2062+
2063+
T = TypeVar('T')
2064+
2065+
def foo(x: T) -> T:
2066+
out = None
2067+
out = x
2068+
if out is None:
2069+
pass
2070+
return out
2071+
[builtins fixtures/isinstance.pyi]
2072+
2073+
[case testNoneCheckDoesNotNarrowWhenUsingTypeVarsNoStrictOptional]
2074+
# flags: --no-strict-optional
2075+
from typing import TypeVar
2076+
2077+
T = TypeVar('T')
2078+
2079+
def foo(x: T) -> T:
2080+
out = None
2081+
out = x
2082+
if out is None:
2083+
pass
2084+
return out
2085+
[builtins fixtures/isinstance.pyi]

0 commit comments

Comments
 (0)