Skip to content
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

Ignore position if imprecise arguments are matched by name #16471

Merged
merged 5 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
24 changes: 18 additions & 6 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1651,7 +1651,12 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
continue
return False
if not are_args_compatible(
left_arg, right_arg, ignore_pos_arg_names, allow_partial_overlap, is_compat
left_arg,
right_arg,
is_compat,
ignore_pos_arg_names=ignore_pos_arg_names,
allow_partial_overlap=allow_partial_overlap,
allow_imprecise_kinds=right.imprecise_arg_kinds,
):
return False

Expand All @@ -1676,9 +1681,9 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
if not are_args_compatible(
left_by_position,
right_by_position,
ignore_pos_arg_names,
allow_partial_overlap,
is_compat,
ignore_pos_arg_names=ignore_pos_arg_names,
allow_partial_overlap=allow_partial_overlap,
):
return False
i += 1
Expand Down Expand Up @@ -1711,7 +1716,11 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
continue

if not are_args_compatible(
left_by_name, right_by_name, ignore_pos_arg_names, allow_partial_overlap, is_compat
left_by_name,
right_by_name,
is_compat,
ignore_pos_arg_names=ignore_pos_arg_names,
allow_partial_overlap=allow_partial_overlap,
):
return False

Expand All @@ -1735,6 +1744,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
and right_by_name != right_by_pos
and (right_by_pos.required or right_by_name.required)
and strict_concatenate_check
and not right.imprecise_arg_kinds
):
return False

Expand All @@ -1749,9 +1759,11 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
def are_args_compatible(
left: FormalArgument,
right: FormalArgument,
is_compat: Callable[[Type, Type], bool],
*,
ignore_pos_arg_names: bool,
allow_partial_overlap: bool,
is_compat: Callable[[Type, Type], bool],
allow_imprecise_kinds: bool = False,
ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
) -> bool:
if left.required and right.required:
# If both arguments are required allow_partial_overlap has no effect.
Expand Down Expand Up @@ -1779,7 +1791,7 @@ def is_different(left_item: object | None, right_item: object | None) -> bool:
return False

# If right is at a specific position, left must have the same:
if is_different(left.pos, right.pos):
if is_different(left.pos, right.pos) and not allow_imprecise_kinds:
return False

# If right's argument is optional, left's must also be
Expand Down
55 changes: 55 additions & 0 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -1687,9 +1687,18 @@ P = ParamSpec("P")
T = TypeVar("T")

def apply(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None: ...

def test(x: int) -> int: ...
apply(apply, test, x=42) # OK
apply(apply, test, 42) # Also OK (but requires some special casing)
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]"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-topic for this PR, and I'm guessing this might be tricky to implement, but it would be really nice if mypy could complain about argument 2 in this error message rather than argument 1. In real-world code, the "mistake" in calls like this is generally passing the wrong kinds of object as arguments after the function, rather than passing in the wrong function altogether

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is really hard.


def test2(x: int, y: str) -> None: ...
apply(apply, test2, 42, "yes")
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]"
apply(apply, test2, x=42, y="yes")
apply(apply, test2, y="yes", x=42)
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]"
[builtins fixtures/paramspec.pyi]

[case testParamSpecApplyPosVsNamedOptional]
Expand Down Expand Up @@ -2086,3 +2095,49 @@ reveal_type(d(b, f1)) # E: Cannot infer type argument 1 of "d" \
# N: Revealed type is "def (*Any, **Any)"
reveal_type(d(b, f2)) # N: Revealed type is "def (builtins.int)"
[builtins fixtures/paramspec.pyi]

[case testParamSpecGenericWithNamedArg1]
from typing import Callable, TypeVar
from typing_extensions import ParamSpec

R = TypeVar("R")
P = ParamSpec("P")

def run(func: Callable[[], R], *args: object, backend: str = "asyncio") -> R: ...
class Result: ...
def run_portal() -> Result: ...
def submit(func: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R: ...

reveal_type(submit( # N: Revealed type is "__main__.Result"
run,
run_portal,
backend="asyncio",
))
submit(
run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], int], Result]"
run_portal,
backend=int(),
)
[builtins fixtures/paramspec.pyi]

[case testParamSpecGenericWithNamedArg2]
from typing import Callable, TypeVar, Type
from typing_extensions import ParamSpec

P= ParamSpec("P")
T = TypeVar("T")

def smoke_testable(*args: P.args, **kwargs: P.kwargs) -> Callable[[Callable[P, T]], Type[T]]:
...

@smoke_testable(name="bob", size=512, flt=0.5)
class SomeClass:
def __init__(self, size: int, name: str, flt: float) -> None:
pass

# Error message is confusing, but this is a known issue, see #4530.
@smoke_testable(name=42, size="bad", flt=0.5) # E: Argument 1 has incompatible type "Type[OtherClass]"; expected "Callable[[int, str, float], OtherClass]"
class OtherClass:
def __init__(self, size: int, name: str, flt: float) -> None:
pass
[builtins fixtures/paramspec.pyi]