-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Optimize is_unsafe_overlapping_overload_signatures for overlapping typevars case #18159
base: master
Are you sure you want to change the base?
Conversation
This comment has been minimized.
This comment has been minimized.
This looks like an important performance fix, but unfortunately seems to change the from __future__ import annotations
from typing import Generic, TypeVar, overload
T1 = TypeVar("T1", int, str, bool)
T2 = TypeVar("T2", int, str, bool)
T3 = TypeVar("T3", int, str, bool)
T4 = TypeVar("T4", int, str, bool)
class A(Generic[T1, T2, T3, T4]):
@overload
def foo(self: A[int, T2, T3, T4]) -> int: ...
@overload
def foo(self: A[int, T2, T3, bool]) -> str: ...
@overload
def foo(self: A[str, T2, T3, T4]) -> str: ...
def foo(self) -> int | str:
return 0 And from __future__ import annotations
from typing import Generic, TypeVar, overload
T1 = TypeVar("T1", int, str, bool)
T2 = TypeVar("T2", int, str, bool)
T3 = TypeVar("T3", int, str, bool)
T4 = TypeVar("T4", int, str, bool)
class A(Generic[T1, T2, T3, T4]):
@overload
def foo(self: A[int, T2, T3, bool]) -> str: ...
@overload
def foo(self: A[int, T2, T3, T4]) -> int: ...
@overload
def foo(self: A[str, T2, T3, T4]) -> str: ...
def foo(self) -> int | str:
return 0 (they only differ in overloads order) Current mypy
in both cases. With this patch the first one is green (which is invalid) and the second one emits
|
This comment has been minimized.
This comment has been minimized.
@sterliakov Initially I thought that we get from typing import Generic, TypeVar, Union, overload
class A:
pass
class B(A):
pass
T1 = TypeVar("T1", A, B)
T2 = TypeVar("T2", A, B)
class T(Generic[T1, T2]):
@overload
def foo(self: T[T1, T2]) -> int: ...
@overload
def foo(self: T[T1, A]) -> str: ... # same for flip signatures case
def foo(self) -> Union[int, str]:
raise NotImplementedError Actual variant that triggers the error: @overload
def foo(self: T[A, B]) -> int: ...
@overload
def foo(self: T[B, A]) -> str: ... alternative example where `B` is not a subtype of `A` does not raise any errorsfrom typing import Generic, TypeVar, Union, overload
class A:
pass
class B:
pass
T1 = TypeVar("T1", A, B)
T2 = TypeVar("T2", A, B)
class T(Generic[T1, T2]):
@overload
def foo(self: T[T1, T2]) -> int: ...
@overload
def foo(self: T[T1, A]) -> str: ... # same for flip signatures case
def foo(self) -> Union[int, str]:
raise NotImplementedError It seems that mypy behavior here (at least according to testOverloadedPartiallyOverlappingTypeVarsAndUnion test case) is to assume that self type is covariant and to allow overload signatures to shadow later narrower signatures for some variants of generic. I am actually not sure what is the correct behavior here. Currently this one seems most consistent to me:
Maybe it is a good idea to allow only strictly narrower function signature variants to shadow later ones to prevent cases when there is a pair of signature variants with exactly same arguments but incompatible return types? I've changed the last check in |
Could you illustrate this with some example? As far as I understand, you suggest the following to become allowed: from typing import overload
class A: pass
class B(A): pass
@overload
def fn(x: B) -> str: ...
@overload
def fn(x: A) -> float: ...
def fn(x: A) -> str | float:
return "" if isinstance(x, B) else 0 It's quite important to report def handle(t: A) -> float:
return fn(t) + 1
handle(B()) BTW, there's a bug in current |
Not really, sorry for the confusion, I meant that strictly wider in terms of the arguments earlier overload signatures should shadow the later ones (emphasis on strictly here, otherwise it is just the current behavior), so:
@overload
def g(x: Union[B, C]) -> int: ...
@overload
def g(x: B) -> str: ...
def g(x): pass
class A: pass
class B: pass
T = TypeVar('T', A, B)
@overload
def f(x: T) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f(x: B) -> str: ...
def f(x): ...
T = TypeVar('T', A, B)
@overload
def f(x: T) -> int: ...
@overload
def f(x: B = ...) -> str: ...
def f(x): ... |
Ohh. I have to ask: why don't we produce "signature 2 will never be matched [overload-cannot-match]" there? It is indeed type-safe as the second overload is just ignored entirely, but also indicates a programming error. I don't think |
TBH, I think changes in this PR improve |
The problem is that |
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
Fixes #18022
This change optimizes
is_unsafe_overlapping_overload_signatures
so overloads containing many type variables can be checked faster.Let:
It seems that currently typechecking a functions with overloads have$O(N^2V^{2K})$ time complexity:
If I am not mistaken (please correct me if I am) it is not necessary to check pairs of overrides where same type variable is expanded to different variants across different signatures, so we can expand variables jointly for both signatures reducing time complexity to$O(N^2V^K)$
On my original example (using 6 type variables) this change gives 40x time improvement:
Example code snippet
py-spy results without change
py-spy results after change