diff --git a/mypy/constraints.py b/mypy/constraints.py index 079f6536ee20..bd25fdfbd4fa 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -136,6 +136,10 @@ def infer_constraints_for_callable( incomplete_star_mapping = True # type: ignore[unreachable] break + # some constraints are more likely right than others + # so we store them separately and remove unlikely ones later + priority_constraints = [] + for i, actuals in enumerate(formal_to_actual): if isinstance(callee.arg_types[i], UnpackType): unpack_type = callee.arg_types[i] @@ -178,7 +182,7 @@ def infer_constraints_for_callable( ) if isinstance(unpacked_type, TypeVarTupleType): - constraints.append( + priority_constraints.append( Constraint( unpacked_type, SUPERTYPE_OF, @@ -273,7 +277,15 @@ def infer_constraints_for_callable( if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - return constraints + + # TODO: consider passing this up the call stack + for tv in {c.origin_type_var for c in priority_constraints}: + from mypy.solve import solve_constraints + + if solve_constraints([tv], constraints + priority_constraints)[0][0] is None: + constraints = [c for c in constraints if c.type_var != tv.id] + + return constraints + priority_constraints def infer_constraints( @@ -1108,6 +1120,13 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: # (with literal '...'). if not template.is_ellipsis_args: unpack_present = find_unpack_in_list(template.arg_types) + + # TODO: do we need some special-casing when unpack is present in actual + # callable but not in template callable? + res.extend( + infer_callable_arguments_constraints(template, cactual, self.direction) + ) + # When both ParamSpec and TypeVarTuple are present, things become messy # quickly. For now, we only allow ParamSpec to "capture" TypeVarTuple, # but not vice versa. @@ -1125,12 +1144,6 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: template_types, actual_types, neg_op(self.direction) ) res.extend(unpack_constraints) - else: - # TODO: do we need some special-casing when unpack is present in actual - # callable but not in template callable? - res.extend( - infer_callable_arguments_constraints(template, cactual, self.direction) - ) else: prefix = param_spec.prefix prefix_len = len(prefix.arg_types) @@ -1464,6 +1477,7 @@ def repack_callable_args(callable: CallableType, tuple_type: TypeInfo) -> list[T list with unpack in the middle, and prefix/suffix on the sides (as they would appear in e.g. a TupleType). """ + # TODO: don't repack kw-only args, e.g. with `(a: int, *, b: int)` if ARG_STAR not in callable.arg_kinds: return callable.arg_types star_index = callable.arg_kinds.index(ARG_STAR) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 71b8b0ba59f5..403b92e8f79c 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1663,15 +1663,16 @@ def are_parameters_compatible( trivial_vararg_suffix = False if ( - right.arg_kinds[-1:] == [ARG_STAR] - and isinstance(get_proper_type(right.arg_types[-1]), AnyType) + right_star is not None + and isinstance(get_proper_type(right_star.typ), AnyType) and not is_proper_subtype - and all(k.is_positional(star=True) for k in left.arg_kinds) ): # Similar to how (*Any, **Any) is considered a supertype of all callables, we consider # (*Any) a supertype of all callables with positional arguments. This is needed in # particular because we often refuse to try type inference if actual type is not # a subtype of erased template type. + + # This case also ensures that *Any is any length. trivial_vararg_suffix = True # Match up corresponding arguments and check them for compatibility. In diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index d364439f22e9..ea71963adae6 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2312,18 +2312,18 @@ def good2(*args: str) -> int: ... # These are special-cased for *args: Any (as opposite to *args: object) def ok1(a: str, b: int, /) -> None: ... def ok2(c: bytes, *args: int) -> str: ... +def ok3(**kwargs: None) -> None: ... def bad1(*, d: str) -> int: ... -def bad2(**kwargs: None) -> None: ... higher_order(good1) higher_order(good2) higher_order(ok1) higher_order(ok2) +higher_order(ok3) higher_order(bad1) # E: Argument 1 to "higher_order" has incompatible type "Callable[[NamedArg(str, 'd')], int]"; expected "Callable[[VarArg(Any)], Any]" -higher_order(bad2) # E: Argument 1 to "higher_order" has incompatible type "Callable[[KwArg(None)], None]"; expected "Callable[[VarArg(Any)], Any]" [builtins fixtures/tuple.pyi] [case testAliasToCallableWithUnpack2] @@ -2382,10 +2382,10 @@ def func(x: Array[Unpack[Ts]], *args: Unpack[Ts]) -> Tuple[Unpack[Ts]]: def a2(x: Array[int, str]) -> None: reveal_type(func(x, 2, "Hello")) # N: Revealed type is "Tuple[builtins.int, builtins.str]" - reveal_type(func(x, 2)) # E: Cannot infer type argument 1 of "func" \ - # N: Revealed type is "builtins.tuple[Any, ...]" - reveal_type(func(x, 2, "Hello", True)) # E: Cannot infer type argument 1 of "func" \ - # N: Revealed type is "builtins.tuple[Any, ...]" + reveal_type(func(x, 2)) # N: Revealed type is "Tuple[Literal[2]?]" \ + # E: Argument 1 to "func" has incompatible type "Array[int, str]"; expected "Array[int]" + reveal_type(func(x, 2, "Hello", True)) # N: Revealed type is "Tuple[Literal[2]?, Literal['Hello']?, Literal[True]?]" \ + # E: Argument 1 to "func" has incompatible type "Array[int, str]"; expected "Array[int, str, bool]" [builtins fixtures/tuple.pyi] [case testTypeVarTupleTypeApplicationOverload] @@ -2628,3 +2628,37 @@ def fn(f: Callable[[*tuple[T]], int]) -> Callable[[*tuple[T]], int]: ... def test(*args: Unpack[tuple[T]]) -> int: ... reveal_type(fn(test)) # N: Revealed type is "def [T] (T`1) -> builtins.int" [builtins fixtures/tuple.pyi] + +[case testKwargWithTypeVarTupleInference] +# https://github.com/python/mypy/issues/16522 +from typing import Generic, TypeVar, Protocol +from typing_extensions import TypeVarTuple, Unpack + +PosArgT = TypeVarTuple("PosArgT") +StatusT = TypeVar("StatusT") +StatusT_co = TypeVar("StatusT_co", covariant=True) +StatusT_contra = TypeVar("StatusT_contra", contravariant=True) + +class TaskStatus(Generic[StatusT_contra]): + def started(self, value: StatusT_contra) -> None: ... + +class NurseryStartFunc(Protocol[Unpack[PosArgT], StatusT_co]): + def __call__( + self, + *args: Unpack[PosArgT], + task_status: TaskStatus[StatusT_co], + ) -> object: ... + +def nursery_start( + async_fn: NurseryStartFunc[Unpack[PosArgT], StatusT], + *args: Unpack[PosArgT], +) -> StatusT: ... + +def task(a: int, b: str, *, task_status: TaskStatus[str]) -> None: ... + +def test() -> None: + reveal_type(nursery_start(task, "a", 2)) # N: Revealed type is "builtins.str" \ + # E: Argument 1 to "nursery_start" has incompatible type "Callable[[int, str, NamedArg(TaskStatus[str], 'task_status')], None]"; expected "NurseryStartFunc[str, int, str]" \ + # N: "NurseryStartFunc[str, int, str].__call__" has type "Callable[[str, int, NamedArg(TaskStatus[str], 'task_status')], object]" + reveal_type(nursery_start(task, 1, "b")) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi]