Skip to content

Commit

Permalink
Fix type application for classes with generic constructors (#17354)
Browse files Browse the repository at this point in the history
Fixes #17212

Note I removed the problematic asset after all. It is hard to maintain
it, since this function may be called from both explicit application,
and from type inference code paths. And these two cases may have
different min/max type argument count (see tests and comments for
examples).
  • Loading branch information
ilevkivskyi authored Jun 10, 2024
1 parent 6427da6 commit 4fa4657
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 12 deletions.
5 changes: 2 additions & 3 deletions mypy/applytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ def apply_generic_arguments(
bound or constraints, instead of giving an error.
"""
tvars = callable.variables
min_arg_count = sum(not tv.has_default() for tv in tvars)
assert min_arg_count <= len(orig_types) <= len(tvars)
assert len(orig_types) <= len(tvars)
# Check that inferred type variable values are compatible with allowed
# values and bounds. Also, promote subtype values to allowed values.
# Create a map from type variable id to target type.
Expand Down Expand Up @@ -156,7 +155,7 @@ def apply_generic_arguments(
type_is = None

# The callable may retain some type vars if only some were applied.
# TODO: move apply_poly() logic from checkexpr.py here when new inference
# TODO: move apply_poly() logic here when new inference
# becomes universally used (i.e. in all passes + in unification).
# With this new logic we can actually *add* some new free variables.
remaining_tvars: list[TypeVarLikeType] = []
Expand Down
41 changes: 32 additions & 9 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4781,7 +4781,11 @@ class C(Generic[T, Unpack[Ts]]): ...
We simply group the arguments that need to go into Ts variable into a TupleType,
similar to how it is done in other places using split_with_prefix_and_suffix().
"""
vars = t.variables
if t.is_type_obj():
# Type arguments must map to class type variables, ignoring constructor vars.
vars = t.type_object().defn.type_vars
else:
vars = list(t.variables)
args = flatten_nested_tuples(args)

# TODO: this logic is duplicated with semanal_typeargs.
Expand All @@ -4799,6 +4803,7 @@ class C(Generic[T, Unpack[Ts]]): ...

if not vars or not any(isinstance(v, TypeVarTupleType) for v in vars):
return list(args)
# TODO: in future we may want to support type application to variadic functions.
assert t.is_type_obj()
info = t.type_object()
# We reuse the logic from semanal phase to reduce code duplication.
Expand Down Expand Up @@ -4832,10 +4837,23 @@ def apply_type_arguments_to_callable(
tp = get_proper_type(tp)

if isinstance(tp, CallableType):
min_arg_count = sum(not v.has_default() for v in tp.variables)
has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in tp.variables)
if tp.is_type_obj():
# If we have a class object in runtime context, then the available type
# variables are those of the class, we don't include additional variables
# of the constructor. So that with
# class C(Generic[T]):
# def __init__(self, f: Callable[[S], T], x: S) -> None
# C[int] is valid
# C[int, str] is invalid (although C as a callable has 2 type variables)
# Note: various logic below and in applytype.py relies on the fact that
# class type variables appear *before* constructor variables.
type_vars = tp.type_object().defn.type_vars
else:
type_vars = list(tp.variables)
min_arg_count = sum(not v.has_default() for v in type_vars)
has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in type_vars)
if (
len(args) < min_arg_count or len(args) > len(tp.variables)
len(args) < min_arg_count or len(args) > len(type_vars)
) and not has_type_var_tuple:
if tp.is_type_obj() and tp.type_object().fullname == "builtins.tuple":
# e.g. expression tuple[X, Y]
Expand All @@ -4854,19 +4872,24 @@ def apply_type_arguments_to_callable(
bound_args=tp.bound_args,
)
self.msg.incompatible_type_application(
min_arg_count, len(tp.variables), len(args), ctx
min_arg_count, len(type_vars), len(args), ctx
)
return AnyType(TypeOfAny.from_error)
return self.apply_generic_arguments(tp, self.split_for_callable(tp, args, ctx), ctx)
if isinstance(tp, Overloaded):
for it in tp.items:
min_arg_count = sum(not v.has_default() for v in it.variables)
has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in it.variables)
if tp.is_type_obj():
# Same as above.
type_vars = tp.type_object().defn.type_vars
else:
type_vars = list(it.variables)
min_arg_count = sum(not v.has_default() for v in type_vars)
has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in type_vars)
if (
len(args) < min_arg_count or len(args) > len(it.variables)
len(args) < min_arg_count or len(args) > len(type_vars)
) and not has_type_var_tuple:
self.msg.incompatible_type_application(
min_arg_count, len(it.variables), len(args), ctx
min_arg_count, len(type_vars), len(args), ctx
)
return AnyType(TypeOfAny.from_error)
return Overloaded(
Expand Down
6 changes: 6 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2376,6 +2376,12 @@ def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: bool) -
if not t.args:
if not (empty_tuple_index and len(t.type.type_vars) == 1):
# The Any arguments should be set by the caller.
if empty_tuple_index and min_tv_count:
fail(
f"At least {min_tv_count} type argument(s) expected, none given",
t,
code=codes.TYPE_ARG,
)
return False
elif not correct:
fail(
Expand Down
13 changes: 13 additions & 0 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -3443,6 +3443,19 @@ h: Callable[[Unpack[Us]], Foo[int]]
reveal_type(dec(h)) # N: Revealed type is "def (builtins.int) -> __main__.Foo[builtins.int]"
[builtins fixtures/list.pyi]

[case testTypeApplicationGenericConstructor]
from typing import Generic, TypeVar, Callable

T = TypeVar("T")
S = TypeVar("S")
class C(Generic[T]):
def __init__(self, f: Callable[[S], T], x: S) -> None:
self.x = f(x)

reveal_type(C[int]) # N: Revealed type is "def [S] (f: def (S`-1) -> builtins.int, x: S`-1) -> __main__.C[builtins.int]"
Alias = C[int]
C[int, str] # E: Type application has too many types (1 expected)

[case testHigherOrderGenericPartial]
from typing import TypeVar, Callable

Expand Down
27 changes: 27 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -2378,3 +2378,30 @@ def a2(x: Array[int, str]) -> None:
reveal_type(func(x, 2, "Hello", True)) # E: Cannot infer type argument 1 of "func" \
# N: Revealed type is "builtins.tuple[Any, ...]"
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleTypeApplicationOverload]
from typing import Generic, TypeVar, TypeVarTuple, Unpack, overload, Callable

T = TypeVar("T")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
Ts = TypeVarTuple("Ts")

class C(Generic[T, Unpack[Ts]]):
@overload
def __init__(self, f: Callable[[Unpack[Ts]], T]) -> None: ...
@overload
def __init__(self, f: Callable[[T1, T2, T3, Unpack[Ts]], T], a: T1, b: T2, c: T3) -> None: ...
def __init__(self, f, *args, **kwargs) -> None:
...

reveal_type(C[int, str]) # N: Revealed type is "Overload(def (f: def (builtins.str) -> builtins.int) -> __main__.C[builtins.int, builtins.str], def [T1, T2, T3] (f: def (T1`-1, T2`-2, T3`-3, builtins.str) -> builtins.int, a: T1`-1, b: T2`-2, c: T3`-3) -> __main__.C[builtins.int, builtins.str])"
Alias = C[int, str]

def f(x: int, y: int, z: int, t: int) -> str: ...
x = C(f, 0, 0, "hm") # E: Argument 1 to "C" has incompatible type "Callable[[int, int, int, int], str]"; expected "Callable[[int, int, str, int], str]"
reveal_type(x) # N: Revealed type is "__main__.C[builtins.str, builtins.int]"
reveal_type(C(f)) # N: Revealed type is "__main__.C[builtins.str, builtins.int, builtins.int, builtins.int, builtins.int]"
C[()] # E: At least 1 type argument(s) expected, none given
[builtins fixtures/tuple.pyi]

0 comments on commit 4fa4657

Please sign in to comment.