Skip to content

Commit eab6e5a

Browse files
committed
Fix invalid contextual inference for generic call arguments
1 parent 30099ec commit eab6e5a

3 files changed

Lines changed: 232 additions & 9 deletions

File tree

‎mypy/checkexpr.py‎

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,16 +1789,38 @@ def check_callable_call(
17891789
callee = freshen_function_type_vars(callee)
17901790
self.freshen_cache[(context, original_callee)] = callee
17911791

1792+
formal_to_actual: list[list[int]] | None = None
17921793
if callee.is_generic():
1794+
callee_before_context = callee
17931795
callee = self.infer_function_type_arguments_using_context(callee, context)
1796+
if callee != callee_before_context:
1797+
formal_to_actual = map_actuals_to_formals(
1798+
arg_kinds,
1799+
arg_names,
1800+
callee.arg_kinds,
1801+
callee.arg_names,
1802+
lambda i: self.accept(args[i]),
1803+
)
1804+
if self.should_infer_args_without_context(
1805+
callee_before_context,
1806+
callee,
1807+
args,
1808+
arg_kinds,
1809+
arg_names,
1810+
formal_to_actual,
1811+
context,
1812+
):
1813+
callee = callee_before_context
1814+
formal_to_actual = None
17941815

1795-
formal_to_actual = map_actuals_to_formals(
1796-
arg_kinds,
1797-
arg_names,
1798-
callee.arg_kinds,
1799-
callee.arg_names,
1800-
lambda i: self.accept(args[i]),
1801-
)
1816+
if formal_to_actual is None:
1817+
formal_to_actual = map_actuals_to_formals(
1818+
arg_kinds,
1819+
arg_names,
1820+
callee.arg_kinds,
1821+
callee.arg_names,
1822+
lambda i: self.accept(args[i]),
1823+
)
18021824

18031825
if callee.special_sig == "tuple" and len(args) == 1:
18041826
with self.msg.filter_errors():
@@ -2097,6 +2119,115 @@ def infer_arg_types_in_context(
20972119
res.append(self.accept(arg))
20982120
return res
20992121

2122+
def should_infer_args_without_context(
2123+
self,
2124+
callee: CallableType,
2125+
contextual_callee: CallableType,
2126+
args: list[Expression],
2127+
arg_kinds: list[ArgKind],
2128+
arg_names: Sequence[str | None] | None,
2129+
formal_to_actual: list[list[int]],
2130+
context: Context,
2131+
) -> bool:
2132+
"""Should we abandon contextual type argument inference for this call?
2133+
2134+
Context can infer a wider type argument than the actual argument supports when
2135+
the type variable also appears in an invariant or callback argument position.
2136+
In that case we can use regular argument inference instead, but only if it
2137+
produces a valid call whose return type still satisfies the outer context.
2138+
"""
2139+
if contextual_callee.special_sig is not None or contextual_callee.is_type_obj():
2140+
# Keep the previous behavior for special synthetic signatures and type object
2141+
# calls, where context is often intentionally used to produce better errors.
2142+
return False
2143+
2144+
valid = True
2145+
2146+
def check_arg(
2147+
caller_type: Type,
2148+
original_caller_type: Type,
2149+
caller_kind: ArgKind,
2150+
callee_type: Type,
2151+
n: int,
2152+
m: int,
2153+
callee: CallableType,
2154+
object_type: Type | None,
2155+
context: Context,
2156+
outer_context: Context,
2157+
) -> None:
2158+
nonlocal valid
2159+
if not is_subtype(
2160+
get_proper_type(caller_type),
2161+
get_proper_type(callee_type),
2162+
options=self.chk.options,
2163+
):
2164+
valid = False
2165+
2166+
validation_callee = contextual_callee
2167+
validation_formal_to_actual = formal_to_actual
2168+
with self.msg.filter_errors(), self.chk.local_type_map:
2169+
if validation_callee.is_generic():
2170+
need_refresh = any(
2171+
isinstance(v, (ParamSpecType, TypeVarTupleType))
2172+
for v in validation_callee.variables
2173+
)
2174+
validation_callee = self.infer_function_type_arguments(
2175+
validation_callee,
2176+
args,
2177+
arg_kinds,
2178+
arg_names,
2179+
validation_formal_to_actual,
2180+
need_refresh,
2181+
context,
2182+
)
2183+
if need_refresh:
2184+
validation_formal_to_actual = map_actuals_to_formals(
2185+
arg_kinds,
2186+
arg_names,
2187+
validation_callee.arg_kinds,
2188+
validation_callee.arg_names,
2189+
lambda i: self.accept(args[i]),
2190+
)
2191+
arg_types = self.infer_arg_types_in_context(
2192+
validation_callee, args, arg_kinds, validation_formal_to_actual
2193+
)
2194+
self.check_argument_types(
2195+
arg_types,
2196+
arg_kinds,
2197+
args,
2198+
validation_callee,
2199+
validation_formal_to_actual,
2200+
context,
2201+
check_arg=check_arg,
2202+
)
2203+
if valid:
2204+
return False
2205+
2206+
ctx = self.type_context[-1]
2207+
if not ctx:
2208+
return False
2209+
2210+
with self.msg.filter_errors() as local_errors, self.chk.local_type_map:
2211+
formal_to_actual = map_actuals_to_formals(
2212+
arg_kinds,
2213+
arg_names,
2214+
callee.arg_kinds,
2215+
callee.arg_names,
2216+
lambda i: self.accept(args[i]),
2217+
)
2218+
need_refresh = any(
2219+
isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables
2220+
)
2221+
inferred_callee = self.infer_function_type_arguments(
2222+
callee, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context
2223+
)
2224+
return (
2225+
not local_errors.has_new_errors()
2226+
and not has_ambiguous_uninhabited_component(inferred_callee.ret_type)
2227+
and not has_erased_component(inferred_callee.ret_type)
2228+
and is_subtype(inferred_callee.ret_type, ctx, options=self.chk.options)
2229+
)
2230+
21002231
def infer_function_type_arguments_using_context(
21012232
self, callable: CallableType, error_context: Context
21022233
) -> CallableType:

‎test-data/unit/check-inference-context.test‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,70 @@ def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]:
15421542
return mymin(paths, key=key, default=None)
15431543
[builtins fixtures/tuple.pyi]
15441544

1545+
[case testContextInferenceDoesNotWidenInvariantArgument]
1546+
from typing import Generic, Iterable, Iterator, TypeVar, Union
1547+
1548+
T = TypeVar("T")
1549+
S = TypeVar("S")
1550+
1551+
class Vec(Generic[T]):
1552+
def getitem(self, i: int) -> T: pass
1553+
def setitem(self, i: int, v: T) -> None: pass
1554+
def __init__(self, iterable: Iterable[T]) -> None: pass
1555+
def __iter__(self) -> Iterator[T]: pass
1556+
def __add__(self, value: "Vec[S]") -> "Vec[Union[S, T]]": pass
1557+
1558+
def fmt(arg: Iterable[Union[int, str]]) -> None: pass
1559+
1560+
l1: Vec[int] = Vec([1])
1561+
l2: Vec[int] = Vec([1])
1562+
fmt(l1 + l2)
1563+
1564+
dummy = l1 + l2
1565+
fmt(dummy)
1566+
1567+
[case testContextInferenceDoesNotWidenKeyedMinItem]
1568+
from typing import Callable, Iterable, List, Tuple, TypeVar
1569+
1570+
T = TypeVar("T")
1571+
1572+
def my_min(values: Iterable[T], *, key: Callable[[T], int]) -> T: pass
1573+
def tuple_len(x: Tuple[str, ...]) -> int: pass
1574+
def dot_join(parts: Iterable[str]) -> str: pass
1575+
1576+
def smallest_prefix(x: List[Tuple[str, ...]]) -> str:
1577+
return dot_join(my_min(x, key=tuple_len))
1578+
[builtins fixtures/tuple.pyi]
1579+
1580+
[case testContextFallbackDoesNotUseAmbiguousNeverReturn]
1581+
from typing import Generic, Protocol, TypeVar
1582+
from typing_extensions import TypeVarTuple, Unpack
1583+
1584+
PosArgT = TypeVarTuple("PosArgT")
1585+
StatusT = TypeVar("StatusT")
1586+
StatusT_co = TypeVar("StatusT_co", covariant=True)
1587+
StatusT_contra = TypeVar("StatusT_contra", contravariant=True)
1588+
1589+
class TaskStatus(Generic[StatusT_contra]):
1590+
def started(self, value: StatusT_contra) -> None: ...
1591+
1592+
class NurseryStartFunc(Protocol[Unpack[PosArgT], StatusT_co]):
1593+
def __call__(
1594+
self, *args: Unpack[PosArgT], task_status: TaskStatus[StatusT_co]
1595+
) -> object: ...
1596+
1597+
def nursery_start(
1598+
async_fn: NurseryStartFunc[Unpack[PosArgT], StatusT], *args: Unpack[PosArgT]
1599+
) -> StatusT: ...
1600+
1601+
def task(a: int, b: str, *, task_status: TaskStatus[set[str]]) -> None: ...
1602+
1603+
def test() -> None:
1604+
result: set[str] = nursery_start(task, 1, "b")
1605+
result = nursery_start(task, "a", 2) # E: Argument 1 to "nursery_start" has incompatible type "def task(a: int, b: str, *, task_status: TaskStatus[set[str]]) -> None"; expected "NurseryStartFunc[str, int, set[str]]" \
1606+
# N: "NurseryStartFunc[str, int, set[str]].__call__" has type "def __call__(str, int, /, *, task_status: TaskStatus[set[str]]) -> object"
1607+
[builtins fixtures/set.pyi]
1608+
15451609
[case testBinaryOpInferenceContext]
15461610
from typing import Literal, TypeVar
15471611

@@ -1617,6 +1681,34 @@ async def outer(c: Cls[T]) -> Optional[T]:
16171681
[builtins fixtures/async_await.pyi]
16181682
[typing fixtures/typing-async.pyi]
16191683

1684+
[case testContextInferenceDoesNotWidenAwaitedInferredAttribute]
1685+
from typing import Mapping, Optional, TypeVar
1686+
1687+
K = TypeVar("K")
1688+
1689+
async def select_one(options: Mapping[K, str]) -> Optional[K]:
1690+
return None
1691+
1692+
class ViaInferredAttr:
1693+
async def m(self) -> None:
1694+
cases = {"a": "x"}
1695+
self._done = await select_one(cases)
1696+
1697+
async def via_local() -> None:
1698+
cases = {"a": "x"}
1699+
done = await select_one(cases)
1700+
done = await select_one(cases)
1701+
done2: Optional[str] = await select_one(cases)
1702+
1703+
class ViaAnnotatedAttr:
1704+
_done: Optional[str]
1705+
1706+
async def m(self) -> None:
1707+
cases = {"a": "x"}
1708+
self._done = await select_one(cases)
1709+
[builtins fixtures/dict.pyi]
1710+
[typing fixtures/typing-async.pyi]
1711+
16201712
[case testContextFallbackNarrowingTernaryRValue]
16211713
from typing import Iterable, Union
16221714

‎test-data/unit/check-parameter-specification.test‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,6 +2814,6 @@ x: Union[Any, C[[Any, Any]]]
28142814

28152815
def test(x: int) -> None: ...
28162816

2817-
x = foo(test) # E: Argument 1 to "foo" has incompatible type "Callable[[int], None]"; expected "Callable[[Any, Any], None]"
2818-
reveal_type(x) # N: Revealed type is "__main__.C[[Any, Any]]"
2817+
x = foo(test)
2818+
reveal_type(x) # N: Revealed type is "__main__.C[[x: builtins.int]]"
28192819
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)