From 4fa4657d7d9e41b5b3b7ae093ccdc360ce6e1b95 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 10 Jun 2024 20:40:06 +0100 Subject: [PATCH] Fix type application for classes with generic constructors (#17354) Fixes https://github.com/python/mypy/issues/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). --- mypy/applytype.py | 5 ++- mypy/checkexpr.py | 41 +++++++++++++++++++------ mypy/typeanal.py | 6 ++++ test-data/unit/check-generics.test | 13 ++++++++ test-data/unit/check-typevar-tuple.test | 27 ++++++++++++++++ 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index 4847570b1712..783748cd8a5e 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -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. @@ -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] = [] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 779d63c8d385..c34952b084f9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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. @@ -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. @@ -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] @@ -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( diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 28abd24149e6..82c90272d6c2 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -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( diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index b4b075694bb4..ea3f501fd949 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -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 diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 2751e01aa21a..0aff702e1b22 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -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]