Skip to content

Commit bcac597

Browse files
authored
Make join of callables a bit more principled (#22296)
Fixes #9756 Fixes #10160 Fixes #13182 Fixes #14443 This is needed to de-risk #22286
1 parent b286c11 commit bcac597

4 files changed

Lines changed: 113 additions & 28 deletions

File tree

‎mypy/join.py‎

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@
88
import mypy.typeops
99
from mypy.expandtype import expand_type
1010
from mypy.maptype import map_instance_to_supertype
11-
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY, TypeInfo
11+
from mypy.nodes import (
12+
ARG_NAMED,
13+
ARG_POS,
14+
CONTRAVARIANT,
15+
COVARIANT,
16+
INVARIANT,
17+
VARIANCE_NOT_READY,
18+
ArgKind,
19+
TypeInfo,
20+
)
1221
from mypy.state import state
1322
from mypy.subtypes import (
1423
SubtypeContext,
@@ -328,7 +337,7 @@ def visit_unpack_type(self, t: UnpackType) -> UnpackType:
328337

329338
def visit_parameters(self, t: Parameters) -> ProperType:
330339
if isinstance(self.s, Parameters):
331-
if not is_similar_params(t, self.s):
340+
if not is_similar_callables(t, self.s):
332341
# TODO: it would be prudent to return [*object, **object] instead of Any.
333342
return self.default(self.s)
334343
from mypy.meet import meet_types
@@ -337,6 +346,7 @@ def visit_parameters(self, t: Parameters) -> ProperType:
337346
arg_types=[
338347
meet_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)
339348
],
349+
arg_kinds=combine_arg_kinds(self.s, t),
340350
arg_names=combine_arg_names(self.s, t),
341351
)
342352
else:
@@ -752,24 +762,29 @@ def normalize_callables(s: ProperType, t: ProperType) -> tuple[ProperType, Prope
752762
return s, t
753763

754764

755-
def is_similar_callables(t: CallableType, s: CallableType) -> bool:
756-
"""Return True if t and s have identical numbers of
757-
arguments, default arguments and varargs.
758-
"""
759-
return (
760-
len(t.arg_types) == len(s.arg_types)
761-
and t.min_args == s.min_args
762-
and t.is_var_arg == s.is_var_arg
763-
)
764-
765+
def is_similar_callables(
766+
t: CallableType | Parameters, s: CallableType | Parameters, for_meet: bool = False
767+
) -> bool:
768+
"""Return True if t and s have identical numbers of arguments with similar shape.
765769
766-
def is_similar_params(t: Parameters, s: Parameters) -> bool:
767-
# This matches the logic in is_similar_callables() above.
768-
return (
769-
len(t.arg_types) == len(s.arg_types)
770-
and t.min_args == s.min_args
771-
and (t.var_arg() is not None) == (s.var_arg() is not None)
772-
)
770+
If for_meet is True, the argument names must always match.
771+
"""
772+
if len(t.variables) != len(s.variables):
773+
return False
774+
if len(t.arg_types) != len(s.arg_types):
775+
return False
776+
for i, (t_kind, s_kind) in enumerate(zip(t.arg_kinds, s.arg_kinds)):
777+
if t.arg_names[i] != s.arg_names[i]:
778+
if for_meet or t_kind.is_named():
779+
return False
780+
if t_kind == s_kind:
781+
continue
782+
if t_kind.is_positional() and s_kind.is_positional():
783+
continue
784+
if t_kind.is_named() and s_kind.is_named():
785+
continue
786+
return False
787+
return True
773788

774789

775790
def update_callable_ids(c: CallableType, ids: list[TypeVarId]) -> CallableType:
@@ -803,7 +818,12 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
803818
t, s = match_generic_callables(t, s)
804819
arg_types: list[Type] = []
805820
for i in range(len(t.arg_types)):
806-
arg_types.append(safe_meet(t.arg_types[i], s.arg_types[i]))
821+
if isinstance(get_proper_type(t.arg_types[i]), AnyType):
822+
arg_types.append(t.arg_types[i])
823+
elif isinstance(get_proper_type(s.arg_types[i]), AnyType):
824+
arg_types.append(s.arg_types[i])
825+
else:
826+
arg_types.append(safe_meet(t.arg_types[i], s.arg_types[i]))
807827
# TODO in combine_similar_callables also applies here (names and kinds; user metaclasses)
808828
# The fallback type can be either 'function', 'type', or some user-provided metaclass.
809829
# The result should always use 'function' as a fallback if either operands are using it.
@@ -816,6 +836,7 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
816836
instance_type = join_types(t.instance_type, s.instance_type)
817837
return t.copy_modified(
818838
arg_types=arg_types,
839+
arg_kinds=combine_arg_kinds(t, s),
819840
arg_names=combine_arg_names(t, s),
820841
ret_type=join_types(t.ret_type, s.ret_type),
821842
instance_type=instance_type,
@@ -874,6 +895,7 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
874895
instance_type = join_types(t.instance_type, s.instance_type)
875896
return t.copy_modified(
876897
arg_types=arg_types,
898+
arg_kinds=combine_arg_kinds(t, s),
877899
arg_names=combine_arg_names(t, s),
878900
ret_type=join_types(t.ret_type, s.ret_type),
879901
instance_type=instance_type,
@@ -899,20 +921,35 @@ def combine_arg_names(
899921
Note that the third argument's name is omitted and 't' and 's'
900922
are both valid subtypes of this inferred signature.
901923
902-
Precondition: is_similar_types(t, s) is true.
924+
Precondition: is_similar_callables(t, s) is true.
903925
"""
904-
num_args = len(t.arg_types)
905926
new_names = []
906-
for i in range(num_args):
907-
t_name = t.arg_names[i]
908-
s_name = s.arg_names[i]
909-
if t_name == s_name or t.arg_kinds[i].is_named() or s.arg_kinds[i].is_named():
927+
for t_name, s_name in zip(t.arg_names, s.arg_names):
928+
if t_name == s_name:
910929
new_names.append(t_name)
911930
else:
912931
new_names.append(None)
913932
return new_names
914933

915934

935+
def combine_arg_kinds(t: CallableType | Parameters, s: CallableType | Parameters) -> list[ArgKind]:
936+
"""Produces a list of argument kinds compatible with both callables.
937+
938+
Essentially we make all arguments non-optional (both positional and named).
939+
Precondition: is_similar_callables(t, s) is true.
940+
"""
941+
new_kinds = []
942+
for t_kind, s_kind in zip(t.arg_kinds, s.arg_kinds):
943+
if t_kind == s_kind:
944+
new_kinds.append(t_kind)
945+
elif t_kind.is_positional():
946+
new_kinds.append(ARG_POS)
947+
else:
948+
assert t_kind.is_named()
949+
new_kinds.append(ARG_NAMED)
950+
return new_kinds
951+
952+
916953
def object_from_instance(instance: Instance) -> Instance:
917954
"""Construct the type 'builtins.object' from an instance type."""
918955
# Use the fact that 'object' is always the last class in the mro.

‎mypy/meet.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from mypy import join
66
from mypy.erasetype import erase_type
77
from mypy.maptype import map_instance_to_supertype
8+
from mypy.nodes import ARG_NAMED_OPT, ARG_OPT, ArgKind
89
from mypy.state import state
910
from mypy.subtypes import (
1011
are_parameters_compatible,
@@ -957,7 +958,9 @@ def visit_instance(self, t: Instance) -> ProperType:
957958
return self.default(self.s)
958959

959960
def visit_callable_type(self, t: CallableType) -> ProperType:
960-
if isinstance(self.s, CallableType) and join.is_similar_callables(t, self.s):
961+
if isinstance(self.s, CallableType) and join.is_similar_callables(
962+
t, self.s, for_meet=True
963+
):
961964
if is_equivalent(t, self.s):
962965
return join.combine_similar_callables(t, self.s)
963966
result = meet_similar_callables(t, self.s)
@@ -1259,13 +1262,27 @@ def meet_similar_callables(t: CallableType, s: CallableType) -> CallableType:
12591262
instance_type = meet_types(t.instance_type, s.instance_type)
12601263
return t.copy_modified(
12611264
arg_types=arg_types,
1265+
arg_kinds=combine_arg_kinds_for_meet(t, s),
12621266
ret_type=meet_types(t.ret_type, s.ret_type),
12631267
instance_type=instance_type,
12641268
fallback=fallback,
12651269
name=None,
12661270
)
12671271

12681272

1273+
def combine_arg_kinds_for_meet(t: CallableType, s: CallableType) -> list[ArgKind]:
1274+
new_kinds = []
1275+
for t_kind, s_kind in zip(t.arg_kinds, s.arg_kinds):
1276+
if t_kind == s_kind:
1277+
new_kinds.append(t_kind)
1278+
elif t_kind.is_positional():
1279+
new_kinds.append(ARG_OPT)
1280+
else:
1281+
assert t_kind.is_named()
1282+
new_kinds.append(ARG_NAMED_OPT)
1283+
return new_kinds
1284+
1285+
12691286
def meet_type_list(types: list[Type]) -> Type:
12701287
if not types:
12711288
# This should probably be builtins.object but that is hard to get and

‎test-data/unit/check-classes.test‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9819,3 +9819,32 @@ class A:
98199819
def p(self, value: int) -> None:
98209820
self.x = value
98219821
[builtins fixtures/property.pyi]
9822+
9823+
[case testJoinOfTypeObjects]
9824+
from typing import TypeVar
9825+
9826+
class B:
9827+
def __init__(self, *, y=None) -> None: ...
9828+
class C1(B):
9829+
def __init__(self, *, x) -> None: ...
9830+
class C2(B): ...
9831+
9832+
x = [C1, C2]
9833+
reveal_type(x) # N: Revealed type is "builtins.list[builtins.type]"
9834+
9835+
T = TypeVar("T")
9836+
def join(a: T, b: T) -> T: ...
9837+
reveal_type(join(C1, C2)) # N: Revealed type is "builtins.type"
9838+
9839+
[case testLambdaInferredInCollection]
9840+
from typing import Union
9841+
9842+
def f(x: Union[int, None] = None) -> int: ...
9843+
fs = [f, lambda x: 1, f]
9844+
reveal_type(fs) # N: Revealed type is "builtins.list[def (x: Any) -> builtins.int]"
9845+
9846+
[case testLambdaInferredInCollection2]
9847+
def f(x: int, y: int)-> int: ...
9848+
9849+
fs = [lambda df, x: df.foo, f]
9850+
reveal_type(fs) # N: Revealed type is "builtins.list[def (Any, Any) -> Any]"

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1109,8 +1109,10 @@ def f(*, x: int) -> int: ...
11091109
def g(*, y: int) -> int: ...
11101110
def h(*, x: int) -> int: ...
11111111

1112-
list_1 = [f, g] # E: List item 0 has incompatible type "def f(*, x: int) -> int"; expected "def g(*, y: int) -> int"
1112+
list_1 = [f, g]
1113+
reveal_type(list_1) # N: Revealed type is "builtins.list[builtins.function]"
11131114
list_2 = [f, h]
1115+
reveal_type(list_2) # N: Revealed type is "builtins.list[def (*, x: builtins.int) -> builtins.int]"
11141116
[builtins fixtures/list.pyi]
11151117

11161118

0 commit comments

Comments
 (0)