-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Polymorphic inference: support for parameter specifications and lambdas #15837
Changes from 1 commit
9b4eac8
1959136
e796c6a
177b312
420f60d
d7cfbe9
d4c9146
0af630f
c5c1b76
582a4de
4f8afce
2d21032
59963c4
72da8f5
7a87692
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -952,22 +952,31 @@ def coerce_to_literal(typ: Type) -> Type: | |
|
||
|
||
def get_type_vars(tp: Type) -> list[TypeVarType]: | ||
return tp.accept(TypeVarExtractor()) | ||
return cast("list[TypeVarType]", tp.accept(TypeVarExtractor())) | ||
|
||
|
||
class TypeVarExtractor(TypeQuery[List[TypeVarType]]): | ||
def __init__(self) -> None: | ||
def get_all_type_vars(tp: Type) -> list[TypeVarLikeType]: | ||
# TODO: should we always use this function instead of get_type_vars() above? | ||
return tp.accept(TypeVarExtractor(include_all=True)) | ||
|
||
|
||
class TypeVarExtractor(TypeQuery[List[TypeVarLikeType]]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a visit function for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be in a dedicated PR for |
||
def __init__(self, include_all: bool = False) -> None: | ||
super().__init__(self._merge) | ||
self.include_all = include_all | ||
|
||
def _merge(self, iter: Iterable[list[TypeVarType]]) -> list[TypeVarType]: | ||
def _merge(self, iter: Iterable[list[TypeVarLikeType]]) -> list[TypeVarLikeType]: | ||
out = [] | ||
for item in iter: | ||
out.extend(item) | ||
return out | ||
|
||
def visit_type_var(self, t: TypeVarType) -> list[TypeVarType]: | ||
def visit_type_var(self, t: TypeVarType) -> list[TypeVarLikeType]: | ||
return [t] | ||
|
||
def visit_param_spec(self, t: ParamSpecType) -> list[TypeVarLikeType]: | ||
return [t] if self.include_all else [] | ||
|
||
|
||
def custom_special_method(typ: Type, name: str, check_all: bool = False) -> bool: | ||
"""Does this type have a custom special method such as __format__() or __eq__()? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3035,3 +3035,39 @@ reveal_type(dec1(id2)) # N: Revealed type is "def [S in (builtins.int, builtins | |
reveal_type(dec2(id1)) # N: Revealed type is "def [UC <: __main__.C] (UC`5) -> builtins.list[UC`5]" | ||
reveal_type(dec2(id2)) # N: Revealed type is "def (<nothing>) -> builtins.list[<nothing>]" \ | ||
# E: Argument 1 to "dec2" has incompatible type "Callable[[V], V]"; expected "Callable[[<nothing>], <nothing>]" | ||
|
||
[case testInferenceAgainstGenericParamSpecBasicInList] | ||
# flags: --new-type-inference | ||
from typing import TypeVar, Callable, List, Tuple | ||
from typing_extensions import ParamSpec | ||
|
||
T = TypeVar('T') | ||
P = ParamSpec('P') | ||
U = TypeVar('U') | ||
V = TypeVar('V') | ||
|
||
def dec(f: Callable[P, T]) -> Callable[P, List[T]]: ... | ||
def id(x: U) -> U: ... | ||
def either(x: U, y: U) -> U: ... | ||
def pair(x: U, y: V) -> Tuple[U, V]: ... | ||
reveal_type(dec(id)) # N: Revealed type is "def [T] (x: T`2) -> builtins.list[T`2]" | ||
reveal_type(dec(either)) # N: Revealed type is "def [T] (x: T`4, y: T`4) -> builtins.list[T`4]" | ||
reveal_type(dec(pair)) # N: Revealed type is "def [U, V] (x: U`-1, y: V`-2) -> builtins.list[Tuple[U`-1, V`-2]]" | ||
Comment on lines
+3086
to
+3088
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I pointed this out before but I'll move that comment here cause it's significantly more visible here ^^: I don't really like how the typevars switched from a negative to positive id. While I can't imagine this has any negative (heh) impact, it annoys the pedant in me. (Specifically, positive ids are supposedly for classes, which these are not). But moreso, here specifically, I can see that we're treating directly held typevar (the return type) differently to a typevar stuffed inside a generic type ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is (much) more complicated than that. Positive ids are also generated by This is all indeed unnecessary complicated, but this PR doesn't really adds much to it, and cleaning it up would be quite tricky for a very modest benefit. There is one known problem caused by using these semi-global numeric ids -- accidental id clashes (and half of the complications are to avoid them), but it seems that the consensus is that best solution to fix this is to introduce namespaces, like we already did for class type variables. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, alright. I was basing my assumption based on the docstring for |
||
[builtins fixtures/list.pyi] | ||
|
||
[case testInferenceAgainstGenericParamSpecBasicDeList] | ||
# flags: --new-type-inference | ||
from typing import TypeVar, Callable, List, Tuple | ||
from typing_extensions import ParamSpec | ||
|
||
T = TypeVar('T') | ||
P = ParamSpec('P') | ||
U = TypeVar('U') | ||
V = TypeVar('V') | ||
|
||
def dec(f: Callable[P, List[T]]) -> Callable[P, T]: ... | ||
def id(x: U) -> U: ... | ||
def either(x: U, y: U) -> U: ... | ||
reveal_type(dec(id)) # N: Revealed type is "def [T] (x: builtins.list[T`2]) -> T`2" | ||
reveal_type(dec(either)) # N: Revealed type is "def [T] (x: builtins.list[T`4], y: builtins.list[T`4]) -> T`4" | ||
[builtins fixtures/list.pyi] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6456,7 +6456,7 @@ P = ParamSpec("P") | |
R = TypeVar("R") | ||
|
||
@overload | ||
def func(x: Callable[Concatenate[Any, P], R]) -> Callable[P, R]: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot to check why this error disappeared. I will take a look at it if you think it is important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how important this is, though the error is correct. Maybe (I haven't looked through the code yet) you're saying the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I fixed one of the new overload errors in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this doesn't look like a serious regression. |
||
def func(x: Callable[Concatenate[Any, P], R]) -> Callable[P, R]: ... | ||
@overload | ||
def func(x: Callable[P, R]) -> Callable[Concatenate[str, P], R]: ... | ||
def func(x: Callable[..., R]) -> Callable[..., R]: ... | ||
|
@@ -6474,7 +6474,7 @@ eggs = lambda: 'eggs' | |
reveal_type(func(eggs)) # N: Revealed type is "def (builtins.str) -> builtins.str" | ||
|
||
spam: Callable[..., str] = lambda x, y: 'baz' | ||
reveal_type(func(spam)) # N: Revealed type is "def (*Any, **Any) -> builtins.str" | ||
reveal_type(func(spam)) # N: Revealed type is "def (*Any, **Any) -> Any" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised about this change (just purely looking at output): mypy seems to think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one I understand, there is a rule for overloads that if
then the inferred return type becomes |
||
|
||
[builtins fixtures/paramspec.pyi] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1048,10 +1048,10 @@ class Job(Generic[_P, _T]): | |
def generic_f(x: _T) -> _T: ... | ||
|
||
j = Job(generic_f) | ||
reveal_type(j) # N: Revealed type is "__main__.Job[[x: _T`-1], _T`-1]" | ||
reveal_type(j) # N: Revealed type is "__main__.Job[[x: _T`2], _T`2]" | ||
|
||
jf = j.into_callable() | ||
reveal_type(jf) # N: Revealed type is "def [_T] (x: _T`-1) -> _T`-1" | ||
reveal_type(jf) # N: Revealed type is "def [_T] (x: _T`2) -> _T`2" | ||
reveal_type(jf(1)) # N: Revealed type is "builtins.int" | ||
[builtins fixtures/paramspec.pyi] | ||
|
||
|
@@ -1307,15 +1307,18 @@ reveal_type(bar(C(fn=foo, x=1))) # N: Revealed type is "__main__.C[[x: builtins | |
[builtins fixtures/paramspec.pyi] | ||
|
||
[case testParamSpecClassConstructor] | ||
from typing import ParamSpec, Callable | ||
from typing import ParamSpec, Callable, TypeVar | ||
|
||
P = ParamSpec("P") | ||
|
||
class SomeClass: | ||
def __init__(self, a: str) -> None: | ||
pass | ||
|
||
def func(t: Callable[P, SomeClass], val: Callable[P, SomeClass]) -> None: | ||
def func(t: Callable[P, SomeClass], val: Callable[P, SomeClass]) -> Callable[P, SomeClass]: | ||
pass | ||
|
||
def func_regular(t: Callable[[T], SomeClass], val: Callable[[T], SomeClass]) -> Callable[[T], SomeClass]: | ||
pass | ||
|
||
def constructor(a: str) -> SomeClass: | ||
|
@@ -1324,9 +1327,13 @@ def constructor(a: str) -> SomeClass: | |
def wrong_constructor(a: bool) -> SomeClass: | ||
return SomeClass("a") | ||
|
||
def wrong_name_constructor(b: bool) -> SomeClass: | ||
return SomeClass("a") | ||
|
||
func(SomeClass, constructor) | ||
func(SomeClass, wrong_constructor) # E: Argument 1 to "func" has incompatible type "Type[SomeClass]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], SomeClass]" \ | ||
# E: Argument 2 to "func" has incompatible type "Callable[[bool], SomeClass]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], SomeClass]" | ||
reveal_type(func(SomeClass, wrong_constructor)) # N: Revealed type is "def (a: <nothing>) -> __main__.SomeClass" | ||
reveal_type(func_regular(SomeClass, wrong_constructor)) # N: Revealed type is "def (<nothing>) -> __main__.SomeClass" | ||
func(SomeClass, wrong_name_constructor) # E: Argument 1 to "func" has incompatible type "Type[SomeClass]"; expected "Callable[[<nothing>], SomeClass]" | ||
[builtins fixtures/paramspec.pyi] | ||
|
||
[case testParamSpecInTypeAliasBasic] | ||
|
@@ -1547,5 +1554,5 @@ U = TypeVar("U") | |
def dec(f: Callable[P, T]) -> Callable[P, List[T]]: ... | ||
def test(x: U) -> U: ... | ||
reveal_type(dec) # N: Revealed type is "def [P, T] (f: def (*P.args, **P.kwargs) -> T`-2) -> def (*P.args, **P.kwargs) -> builtins.list[T`-2]" | ||
reveal_type(dec(test)) # N: Revealed type is "def [U] (x: U`-1) -> builtins.list[U`-1]" | ||
reveal_type(dec(test)) # N: Revealed type is "def [T] (x: T`2) -> builtins.list[T`2]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a bit kinda like this?: class A: ...
TA = TypeVar("T", bound=A)
def test_with_bound(x: TA) -> TA: ...
reveal_type(dec(test_with_bound))
dec(test_with_bound)(0) # should error
dec(test_with_bound)(A()) # should be AOK ? I'm a bit concerned about the replacing of type variables here, though I do remember seeing something in an earlier PR about updating bounds so that's probably already handled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, upper bounds should be ~decently handled. I added the test. |
||
[builtins fixtures/paramspec.pyi] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the TODO referring to something like #15320 (...which I still need to fix)? That modifies the
subtypes.py
file but that sounds like what you're referring to...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is about #702 (wow, three-digit issue). I actually wanted to work on this at some point soon. I am not sure I will be able to handle all case, but I guess it should be relatively straightforward to cover ~95% of cases.