Skip to content

Infer type var tuple contents in more situations #18958

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 4 additions & 3 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
@@ -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
46 changes: 40 additions & 6 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
@@ -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]