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

Make joins of callables respect positional parameter names #4920

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 33 additions & 1 deletion mypy/join.py
Expand Up @@ -13,6 +13,7 @@
is_subtype, is_equivalent, is_subtype_ignoring_tvars, is_proper_subtype,
is_protocol_implementation
)
from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT

from mypy import experiments

Expand Down Expand Up @@ -348,7 +349,6 @@ def is_similar_callables(t: CallableType, s: CallableType) -> bool:
"""Return True if t and s have identical numbers of
arguments, default arguments and varargs.
"""

return (len(t.arg_types) == len(s.arg_types) and t.min_args == s.min_args and
t.is_var_arg == s.is_var_arg)

Expand All @@ -366,6 +366,7 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
else:
fallback = s.fallback
return t.copy_modified(arg_types=arg_types,
arg_names=combine_arg_names(t, s),
ret_type=join_types(t.ret_type, s.ret_type),
fallback=fallback,
name=None)
Expand All @@ -383,11 +384,42 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
else:
fallback = s.fallback
return t.copy_modified(arg_types=arg_types,
arg_names=combine_arg_names(t, s),
ret_type=join_types(t.ret_type, s.ret_type),
fallback=fallback,
name=None)


def combine_arg_names(t: CallableType, s: CallableType) -> List[Optional[str]]:
"""Produces a list of argument names compatible with both callables.

For example, suppose 't' and 's' have the following signatures:

- t: (a: int, b: str, X: str) -> None
- s: (a: int, b: str, Y: str) -> None

This function would return ["a", "b", None]. This information
is then used above to compute the join of t and s, which results
in a signature of (a: int, b: str, str) -> None.

Note that the third argument's name is omitted and 't' and 's'
are both valid subtypes of this inferred signature.

Precondition: is_similar_types(t, s) is true.
"""
num_args = len(t.arg_types)
new_names = []
named = (ARG_NAMED, ARG_NAMED_OPT)
for i in range(num_args):
t_name = t.arg_names[i]
s_name = s.arg_names[i]
if t_name == s_name or t.arg_kinds[i] in named or s.arg_kinds[i] in named:
new_names.append(t_name)
else:
new_names.append(None)
return new_names


def object_from_instance(instance: Instance) -> Instance:
"""Construct the type 'builtins.object' from an instance type."""
# Use the fact that 'object' is always the last class in the mro.
Expand Down
48 changes: 47 additions & 1 deletion test-data/unit/check-inference.test
Expand Up @@ -856,13 +856,59 @@ s = s_s() # E: Incompatible types in assignment (expression has type "Set[str]",
[builtins fixtures/set.pyi]

[case testSetWithStarExpr]

s = {1, 2, *(3, 4)}
t = {1, 2, *s}
reveal_type(s) # E: Revealed type is 'builtins.set[builtins.int*]'
reveal_type(t) # E: Revealed type is 'builtins.set[builtins.int*]'
[builtins fixtures/set.pyi]

[case testListLiteralWithFunctionsErasesNames]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe add a test for the join_similar_callables path by joining functions whose argument types differ but have a nontrivial meet. (For example, if the arguments are Union[bytes, float] and Union[str, int], the argument for the joined function should be int)

def f1(x: int) -> int: ...
def g1(y: int) -> int: ...
def h1(x: int) -> int: ...

list_1 = [f1, g1]
list_2 = [f1, h1]
reveal_type(list_1) # E: Revealed type is 'builtins.list[def (builtins.int) -> builtins.int]'
reveal_type(list_2) # E: Revealed type is 'builtins.list[def (x: builtins.int) -> builtins.int]'

def f2(x: int, z: str) -> int: ...
def g2(y: int, z: str) -> int: ...
def h2(x: int, z: str) -> int: ...

list_3 = [f2, g2]
list_4 = [f2, h2]
reveal_type(list_3) # E: Revealed type is 'builtins.list[def (builtins.int, z: builtins.str) -> builtins.int]'
reveal_type(list_4) # E: Revealed type is 'builtins.list[def (x: builtins.int, z: builtins.str) -> builtins.int]'
[builtins fixtures/list.pyi]

[case testListLiteralWithSimilarFunctionsErasesName]
from typing import Union

class A: ...
class B(A): ...
class C: ...
class D: ...

def f(x: Union[A, C], y: B) -> A: ...
def g(z: Union[B, D], y: A) -> B: ...
def h(x: Union[B, D], y: A) -> B: ...

list_1 = [f, g]
list_2 = [f, h]
reveal_type(list_1) # E: Revealed type is 'builtins.list[def (__main__.B, y: __main__.B) -> __main__.A]'
reveal_type(list_2) # E: Revealed type is 'builtins.list[def (x: __main__.B, y: __main__.B) -> __main__.A]'
[builtins fixtures/list.pyi]

[case testListLiteralWithNameOnlyArgsDoesNotEraseNames]
def f(*, x: int) -> int: ...
def g(*, y: int) -> int: ...
def h(*, x: int) -> int: ...

list_1 = [f, g] # E: List item 0 has incompatible type "Callable[[NamedArg(int, 'x')], int]"; expected "Callable[[NamedArg(int, 'y')], int]"
list_2 = [f, h]
[builtins fixtures/list.pyi]


-- For statements
-- --------------
Expand Down