Skip to content

Commit c6cb3c6

Browse files
Ignore position if imprecise arguments are matched by name (#16471)
Fixes #16405 Fixes #16412 Imprecise argument kinds inference was added a while ago to support various edge cases with `ParamSpec`. This feature required mapping actual kinds to formal kinds, which is in general undecidable. At that time we decided to not add much special-casing, and wait for some real use-cases. So far there are two relevant issues, and it looks like both of them can be fixed with simple special-casing: ignore argument positions in subtyping if arguments can be matched by name. This adds minor unsafety, and generally doesn't look bad, so I think we should go ahead with it. --------- Co-authored-by: Alex Waygood <[email protected]>
1 parent fbb77c3 commit c6cb3c6

File tree

2 files changed

+73
-6
lines changed

2 files changed

+73
-6
lines changed

mypy/subtypes.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,12 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
16511651
continue
16521652
return False
16531653
if not are_args_compatible(
1654-
left_arg, right_arg, ignore_pos_arg_names, allow_partial_overlap, is_compat
1654+
left_arg,
1655+
right_arg,
1656+
is_compat,
1657+
ignore_pos_arg_names=ignore_pos_arg_names,
1658+
allow_partial_overlap=allow_partial_overlap,
1659+
allow_imprecise_kinds=right.imprecise_arg_kinds,
16551660
):
16561661
return False
16571662

@@ -1676,9 +1681,9 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
16761681
if not are_args_compatible(
16771682
left_by_position,
16781683
right_by_position,
1679-
ignore_pos_arg_names,
1680-
allow_partial_overlap,
16811684
is_compat,
1685+
ignore_pos_arg_names=ignore_pos_arg_names,
1686+
allow_partial_overlap=allow_partial_overlap,
16821687
):
16831688
return False
16841689
i += 1
@@ -1711,7 +1716,11 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
17111716
continue
17121717

17131718
if not are_args_compatible(
1714-
left_by_name, right_by_name, ignore_pos_arg_names, allow_partial_overlap, is_compat
1719+
left_by_name,
1720+
right_by_name,
1721+
is_compat,
1722+
ignore_pos_arg_names=ignore_pos_arg_names,
1723+
allow_partial_overlap=allow_partial_overlap,
17151724
):
17161725
return False
17171726

@@ -1735,6 +1744,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
17351744
and right_by_name != right_by_pos
17361745
and (right_by_pos.required or right_by_name.required)
17371746
and strict_concatenate_check
1747+
and not right.imprecise_arg_kinds
17381748
):
17391749
return False
17401750

@@ -1749,9 +1759,11 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
17491759
def are_args_compatible(
17501760
left: FormalArgument,
17511761
right: FormalArgument,
1762+
is_compat: Callable[[Type, Type], bool],
1763+
*,
17521764
ignore_pos_arg_names: bool,
17531765
allow_partial_overlap: bool,
1754-
is_compat: Callable[[Type, Type], bool],
1766+
allow_imprecise_kinds: bool = False,
17551767
) -> bool:
17561768
if left.required and right.required:
17571769
# If both arguments are required allow_partial_overlap has no effect.
@@ -1779,7 +1791,7 @@ def is_different(left_item: object | None, right_item: object | None) -> bool:
17791791
return False
17801792

17811793
# If right is at a specific position, left must have the same:
1782-
if is_different(left.pos, right.pos):
1794+
if is_different(left.pos, right.pos) and not allow_imprecise_kinds:
17831795
return False
17841796

17851797
# If right's argument is optional, left's must also be

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,9 +1687,18 @@ P = ParamSpec("P")
16871687
T = TypeVar("T")
16881688

16891689
def apply(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None: ...
1690+
16901691
def test(x: int) -> int: ...
16911692
apply(apply, test, x=42) # OK
16921693
apply(apply, test, 42) # Also OK (but requires some special casing)
1694+
apply(apply, test, "bad") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int], int], str], None]"
1695+
1696+
def test2(x: int, y: str) -> None: ...
1697+
apply(apply, test2, 42, "yes")
1698+
apply(apply, test2, "no", 42) # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], str, int], None]"
1699+
apply(apply, test2, x=42, y="yes")
1700+
apply(apply, test2, y="yes", x=42)
1701+
apply(apply, test2, y=42, x="no") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], int, str], None]"
16931702
[builtins fixtures/paramspec.pyi]
16941703

16951704
[case testParamSpecApplyPosVsNamedOptional]
@@ -2087,6 +2096,52 @@ reveal_type(d(b, f1)) # E: Cannot infer type argument 1 of "d" \
20872096
reveal_type(d(b, f2)) # N: Revealed type is "def (builtins.int)"
20882097
[builtins fixtures/paramspec.pyi]
20892098

2099+
[case testParamSpecGenericWithNamedArg1]
2100+
from typing import Callable, TypeVar
2101+
from typing_extensions import ParamSpec
2102+
2103+
R = TypeVar("R")
2104+
P = ParamSpec("P")
2105+
2106+
def run(func: Callable[[], R], *args: object, backend: str = "asyncio") -> R: ...
2107+
class Result: ...
2108+
def run_portal() -> Result: ...
2109+
def submit(func: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R: ...
2110+
2111+
reveal_type(submit( # N: Revealed type is "__main__.Result"
2112+
run,
2113+
run_portal,
2114+
backend="asyncio",
2115+
))
2116+
submit(
2117+
run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], int], Result]"
2118+
run_portal,
2119+
backend=int(),
2120+
)
2121+
[builtins fixtures/paramspec.pyi]
2122+
2123+
[case testParamSpecGenericWithNamedArg2]
2124+
from typing import Callable, TypeVar, Type
2125+
from typing_extensions import ParamSpec
2126+
2127+
P= ParamSpec("P")
2128+
T = TypeVar("T")
2129+
2130+
def smoke_testable(*args: P.args, **kwargs: P.kwargs) -> Callable[[Callable[P, T]], Type[T]]:
2131+
...
2132+
2133+
@smoke_testable(name="bob", size=512, flt=0.5)
2134+
class SomeClass:
2135+
def __init__(self, size: int, name: str, flt: float) -> None:
2136+
pass
2137+
2138+
# Error message is confusing, but this is a known issue, see #4530.
2139+
@smoke_testable(name=42, size="bad", flt=0.5) # E: Argument 1 has incompatible type "Type[OtherClass]"; expected "Callable[[int, str, float], OtherClass]"
2140+
class OtherClass:
2141+
def __init__(self, size: int, name: str, flt: float) -> None:
2142+
pass
2143+
[builtins fixtures/paramspec.pyi]
2144+
20902145
[case testInferenceAgainstGenericCallableUnionParamSpec]
20912146
from typing import Callable, TypeVar, List, Union
20922147
from typing_extensions import ParamSpec

0 commit comments

Comments
 (0)