Skip to content

Commit c5de2fd

Browse files
Michael0x2ailevkivskyi
authored andcommitted
Make overload impl checks correctly handle TypeVars and untyped impls (#5236)
* Make overload impl checks correctly handle TypeVars and untyped impls This pull request fixes #4619 as well as an unrelated bug where if the overload implementation was untyped, we only check to see if there are unsafe overlaps between the *first* overload alternative and the rest. * Remove unify_generics parameter
1 parent 4c3f800 commit c5de2fd

File tree

4 files changed

+114
-37
lines changed

4 files changed

+114
-37
lines changed

mypy/checker.py

+39-29
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
restrict_subtype_away, is_subtype_ignoring_tvars, is_callable_compatible,
4343
unify_generic_callable, find_member
4444
)
45+
from mypy.constraints import SUPERTYPE_OF
4546
from mypy.maptype import map_instance_to_supertype
4647
from mypy.typevars import fill_typevars, has_no_typevars
4748
from mypy.semanal import set_callable_name, refers_to_fullname, calculate_mro
@@ -414,6 +415,23 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
414415
def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
415416
# At this point we should have set the impl already, and all remaining
416417
# items are decorators
418+
419+
# Compute some info about the implementation (if it exists) for use below
420+
impl_type = None # type: Optional[CallableType]
421+
if defn.impl:
422+
if isinstance(defn.impl, FuncDef):
423+
inner_type = defn.impl.type
424+
elif isinstance(defn.impl, Decorator):
425+
inner_type = defn.impl.var.type
426+
else:
427+
assert False, "Impl isn't the right type"
428+
429+
# This can happen if we've got an overload with a different
430+
# decorator or if the implementation is untyped -- we gave up on the types.
431+
if inner_type is not None and not isinstance(inner_type, AnyType):
432+
assert isinstance(inner_type, CallableType)
433+
impl_type = inner_type
434+
417435
is_descriptor_get = defn.info is not None and defn.name() == "__get__"
418436
for i, item in enumerate(defn.items):
419437
# TODO overloads involving decorators
@@ -451,43 +469,35 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
451469
self.msg.overloaded_signatures_overlap(
452470
i + 1, i + j + 2, item.func)
453471

454-
if defn.impl:
455-
if isinstance(defn.impl, FuncDef):
456-
impl_type = defn.impl.type
457-
elif isinstance(defn.impl, Decorator):
458-
impl_type = defn.impl.var.type
459-
else:
460-
assert False, "Impl isn't the right type"
472+
if impl_type is not None:
473+
assert defn.impl is not None
461474

462-
# This can happen if we've got an overload with a different
463-
# decorator too -- we gave up on the types.
464-
if impl_type is None or isinstance(impl_type, AnyType):
465-
return
466-
assert isinstance(impl_type, CallableType)
475+
# We perform a unification step that's very similar to what
476+
# 'is_callable_compatible' would have done if we had set
477+
# 'unify_generics' to True -- the only difference is that
478+
# we check and see if the impl_type's return value is a
479+
# *supertype* of the overload alternative, not a *subtype*.
480+
#
481+
# This is to match the direction the implementation's return
482+
# needs to be compatible in.
483+
if impl_type.variables:
484+
impl = unify_generic_callable(impl_type, sig1,
485+
ignore_return=False,
486+
return_constraint_direction=SUPERTYPE_OF)
487+
if impl is None:
488+
self.msg.overloaded_signatures_typevar_specific(i + 1, defn.impl)
489+
continue
490+
else:
491+
impl = impl_type
467492

468493
# Is the overload alternative's arguments subtypes of the implementation's?
469-
if not is_callable_compatible(impl_type, sig1,
494+
if not is_callable_compatible(impl, sig1,
470495
is_compat=is_subtype,
471496
ignore_return=True):
472497
self.msg.overloaded_signatures_arg_specific(i + 1, defn.impl)
473498

474-
# Repeat the same unification process 'is_callable_compatible'
475-
# internally performs so we can examine the return type separately.
476-
if impl_type.variables:
477-
# Note: we set 'ignore_return=True' because 'unify_generic_callable'
478-
# normally checks the arguments and return types with differing variance.
479-
#
480-
# This is normally what we want, but for checking the validity of overload
481-
# implementations, we actually want to use the same variance for both.
482-
#
483-
# TODO: Patch 'is_callable_compatible' and 'unify_generic_callable'?
484-
# somehow so we can customize the variance in all different sorts
485-
# of ways? This would let us infer more constraints, letting us
486-
# infer more precise types.
487-
impl_type = unify_generic_callable(impl_type, sig1, ignore_return=True)
488-
489499
# Is the overload alternative's return type a subtype of the implementation's?
490-
if impl_type is not None and not is_subtype(sig1.ret_type, impl_type.ret_type):
500+
if not is_subtype(sig1.ret_type, impl.ret_type):
491501
self.msg.overloaded_signatures_ret_specific(i + 1, defn.impl)
492502

493503
# Here's the scoop about generators and coroutines.

mypy/messages.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -967,13 +967,17 @@ def overloaded_signature_will_never_match(self, index1: int, index2: int,
967967
index2=index2),
968968
context)
969969

970-
def overloaded_signatures_arg_specific(self, index1: int, context: Context) -> None:
970+
def overloaded_signatures_typevar_specific(self, index: int, context: Context) -> None:
971+
self.fail('Overloaded function implementation cannot satisfy signature {} '.format(index) +
972+
'due to inconsistencies in how they use type variables', context)
973+
974+
def overloaded_signatures_arg_specific(self, index: int, context: Context) -> None:
971975
self.fail('Overloaded function implementation does not accept all possible arguments '
972-
'of signature {}'.format(index1), context)
976+
'of signature {}'.format(index), context)
973977

974-
def overloaded_signatures_ret_specific(self, index1: int, context: Context) -> None:
978+
def overloaded_signatures_ret_specific(self, index: int, context: Context) -> None:
975979
self.fail('Overloaded function implementation cannot produce return type '
976-
'of signature {}'.format(index1), context)
980+
'of signature {}'.format(index), context)
977981

978982
def operator_method_signatures_overlap(
979983
self, reverse_class: TypeInfo, reverse_method: str, forward_class: Type,

mypy/subtypes.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ def visit_type_var(self, left: TypeVarType) -> bool:
198198
right = self.right
199199
if isinstance(right, TypeVarType) and left.id == right.id:
200200
return True
201+
if left.values and is_subtype(UnionType.make_simplified_union(left.values), right):
202+
return True
201203
return is_subtype(left.upper_bound, self.right)
202204

203205
def visit_callable_type(self, left: CallableType) -> bool:
@@ -901,7 +903,9 @@ def new_is_compat(left: Type, right: Type) -> bool:
901903

902904

903905
def unify_generic_callable(type: CallableType, target: CallableType,
904-
ignore_return: bool) -> Optional[CallableType]:
906+
ignore_return: bool,
907+
return_constraint_direction: int = mypy.constraints.SUBTYPE_OF,
908+
) -> Optional[CallableType]:
905909
"""Try to unify a generic callable type with another callable type.
906910
907911
Return unified CallableType if successful; otherwise, return None.
@@ -914,7 +918,7 @@ def unify_generic_callable(type: CallableType, target: CallableType,
914918
constraints.extend(c)
915919
if not ignore_return:
916920
c = mypy.constraints.infer_constraints(
917-
type.ret_type, target.ret_type, mypy.constraints.SUBTYPE_OF)
921+
type.ret_type, target.ret_type, return_constraint_direction)
918922
constraints.extend(c)
919923
type_var_ids = [tvar.id for tvar in type.variables]
920924
inferred_vars = mypy.solve.solve_constraints(type_var_ids, constraints)
@@ -1036,7 +1040,8 @@ def check_argument(leftarg: Type, rightarg: Type, variance: int) -> bool:
10361040
def visit_type_var(self, left: TypeVarType) -> bool:
10371041
if isinstance(self.right, TypeVarType) and left.id == self.right.id:
10381042
return True
1039-
# TODO: Value restrictions
1043+
if left.values and is_subtype(UnionType.make_simplified_union(left.values), self.right):
1044+
return True
10401045
return is_proper_subtype(left.upper_bound, self.right)
10411046

10421047
def visit_callable_type(self, left: CallableType) -> bool:

test-data/unit/check-overloading.test

+59-1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,17 @@ class A: pass
204204
class B: pass
205205
[builtins fixtures/isinstance.pyi]
206206

207+
[case testTypeCheckOverloadWithUntypedImplAndMultipleVariants]
208+
from typing import overload
209+
210+
@overload
211+
def f(x: int) -> str: ...
212+
@overload
213+
def f(x: str) -> int: ... # E: Overloaded function signatures 2 and 3 overlap with incompatible return types
214+
@overload
215+
def f(x: object) -> str: ...
216+
def f(x): ...
217+
207218
[case testTypeCheckOverloadWithImplTooSpecificArg]
208219
from typing import overload, Any
209220

@@ -284,14 +295,61 @@ def f(x: 'A') -> 'A': ...
284295
@overload
285296
def f(x: 'B') -> 'B': ...
286297

287-
def f(x: Union[T, B]) -> T: # E: Overloaded function implementation cannot produce return type of signature 2
298+
def f(x: Union[T, B]) -> T: # E: Overloaded function implementation cannot satisfy signature 2 due to inconsistencies in how they use type variables
288299
...
289300

290301
reveal_type(f(A())) # E: Revealed type is '__main__.A'
291302
reveal_type(f(B())) # E: Revealed type is '__main__.B'
292303

293304
[builtins fixtures/isinstance.pyi]
294305

306+
[case testTypeCheckOverloadImplementationTypeVarWithValueRestriction]
307+
from typing import overload, TypeVar, Union
308+
309+
class A: pass
310+
class B: pass
311+
class C: pass
312+
313+
T = TypeVar('T', A, B)
314+
315+
@overload
316+
def foo(x: T) -> T: ...
317+
@overload
318+
def foo(x: C) -> int: ...
319+
def foo(x: Union[A, B, C]) -> Union[A, B, int]:
320+
if isinstance(x, C):
321+
return 3
322+
else:
323+
return x
324+
325+
@overload
326+
def bar(x: T) -> T: ...
327+
@overload
328+
def bar(x: C) -> int: ...
329+
def bar(x: Union[T, C]) -> Union[T, int]:
330+
if isinstance(x, C):
331+
return 3
332+
else:
333+
return x
334+
335+
[builtins fixtures/isinstancelist.pyi]
336+
337+
[case testTypeCheckOverloadImplementationTypeVarDifferingUsage]
338+
from typing import overload, Union, List, TypeVar
339+
340+
T = TypeVar('T')
341+
342+
@overload
343+
def foo(t: List[T]) -> T: ...
344+
@overload
345+
def foo(t: T) -> T: ...
346+
def foo(t: Union[List[T], T]) -> T:
347+
if isinstance(t, list):
348+
return t[0]
349+
else:
350+
return t
351+
[builtins fixtures/isinstancelist.pyi]
352+
295353
[case testTypeCheckOverloadedFunctionBody]
296354
from foo import *
297355
[file foo.pyi]

0 commit comments

Comments
 (0)