Skip to content

Commit d7d502e

Browse files
Support self-types containing ParamSpec (#15903)
Fixes #14968 Fixes #13911 The fix is simple, as I predicted on Discord, we simply should use `get_all_type_vars()` instead of `get_type_vars()` (that specifically returns only `TypeVarType`). I also use this opportunity to tidy-up code in `bind_self()`, it should be now more readable, and much faster (especially when compiled with mypyc). cc @A5rocks --------- Co-authored-by: Alex Waygood <[email protected]>
1 parent 1db3eb3 commit d7d502e

File tree

2 files changed

+56
-20
lines changed

2 files changed

+56
-20
lines changed

mypy/typeops.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ class B(A): pass
303303
return cast(F, func)
304304
self_param_type = get_proper_type(func.arg_types[0])
305305

306-
variables: Sequence[TypeVarLikeType] = []
306+
variables: Sequence[TypeVarLikeType]
307307
if func.variables and supported_self_type(self_param_type):
308308
from mypy.infer import infer_type_arguments
309309

@@ -312,46 +312,40 @@ class B(A): pass
312312
original_type = erase_to_bound(self_param_type)
313313
original_type = get_proper_type(original_type)
314314

315-
all_ids = func.type_var_ids()
315+
# Find which of method type variables appear in the type of "self".
316+
self_ids = {tv.id for tv in get_all_type_vars(self_param_type)}
317+
self_vars = [tv for tv in func.variables if tv.id in self_ids]
318+
319+
# Solve for these type arguments using the actual class or instance type.
316320
typeargs = infer_type_arguments(
317-
func.variables, self_param_type, original_type, is_supertype=True
321+
self_vars, self_param_type, original_type, is_supertype=True
318322
)
319323
if (
320324
is_classmethod
321-
# TODO: why do we need the extra guards here?
322325
and any(isinstance(get_proper_type(t), UninhabitedType) for t in typeargs)
323326
and isinstance(original_type, (Instance, TypeVarType, TupleType))
324327
):
325-
# In case we call a classmethod through an instance x, fallback to type(x)
328+
# In case we call a classmethod through an instance x, fallback to type(x).
326329
typeargs = infer_type_arguments(
327-
func.variables, self_param_type, TypeType(original_type), is_supertype=True
330+
self_vars, self_param_type, TypeType(original_type), is_supertype=True
328331
)
329332

330-
ids = [tid for tid in all_ids if any(tid == t.id for t in get_type_vars(self_param_type))]
331-
332-
# Technically, some constrains might be unsolvable, make them <nothing>.
333+
# Update the method signature with the solutions found.
334+
# Technically, some constraints might be unsolvable, make them <nothing>.
333335
to_apply = [t if t is not None else UninhabitedType() for t in typeargs]
334-
335-
def expand(target: Type) -> Type:
336-
return expand_type(target, {id: to_apply[all_ids.index(id)] for id in ids})
337-
338-
arg_types = [expand(x) for x in func.arg_types[1:]]
339-
ret_type = expand(func.ret_type)
340-
variables = [v for v in func.variables if v.id not in ids]
336+
func = expand_type(func, {tv.id: arg for tv, arg in zip(self_vars, to_apply)})
337+
variables = [v for v in func.variables if v not in self_vars]
341338
else:
342-
arg_types = func.arg_types[1:]
343-
ret_type = func.ret_type
344339
variables = func.variables
345340

346341
original_type = get_proper_type(original_type)
347342
if isinstance(original_type, CallableType) and original_type.is_type_obj():
348343
original_type = TypeType.make_normalized(original_type.ret_type)
349344
res = func.copy_modified(
350-
arg_types=arg_types,
345+
arg_types=func.arg_types[1:],
351346
arg_kinds=func.arg_kinds[1:],
352347
arg_names=func.arg_names[1:],
353348
variables=variables,
354-
ret_type=ret_type,
355349
bound_args=[original_type],
356350
)
357351
return cast(F, res)

test-data/unit/check-selftype.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,3 +1973,45 @@ class B(A):
19731973
reveal_type(self.x.extra) # N: Revealed type is "builtins.int"
19741974
reveal_type(self.xs[0].extra) # N: Revealed type is "builtins.int"
19751975
[builtins fixtures/list.pyi]
1976+
1977+
[case testSelfTypesWithParamSpecExtract]
1978+
from typing import Any, Callable, Generic, TypeVar
1979+
from typing_extensions import ParamSpec
1980+
1981+
P = ParamSpec("P")
1982+
F = TypeVar("F", bound=Callable[..., Any])
1983+
class Example(Generic[F]):
1984+
def __init__(self, fn: F) -> None:
1985+
...
1986+
def __call__(self: Example[Callable[P, Any]], *args: P.args, **kwargs: P.kwargs) -> None:
1987+
...
1988+
1989+
def test_fn(a: int, b: str) -> None:
1990+
...
1991+
1992+
example = Example(test_fn)
1993+
example() # E: Missing positional arguments "a", "b" in call to "__call__" of "Example"
1994+
example(1, "b") # OK
1995+
[builtins fixtures/list.pyi]
1996+
1997+
[case testSelfTypesWithParamSpecInfer]
1998+
from typing import TypeVar, Protocol, Type, Callable
1999+
from typing_extensions import ParamSpec
2000+
2001+
R = TypeVar("R", covariant=True)
2002+
P = ParamSpec("P")
2003+
class AsyncP(Protocol[P]):
2004+
def meth(self, *args: P.args, **kwargs: P.kwargs) -> None:
2005+
...
2006+
2007+
class Async:
2008+
@classmethod
2009+
def async_func(cls: Type[AsyncP[P]]) -> Callable[P, int]:
2010+
...
2011+
2012+
class Add(Async):
2013+
def meth(self, x: int, y: int) -> None: ...
2014+
2015+
reveal_type(Add.async_func()) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
2016+
reveal_type(Add().async_func()) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
2017+
[builtins fixtures/classmethod.pyi]

0 commit comments

Comments
 (0)