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

[WIP] Improvement of mismatching type variable error message #3911

Closed
wants to merge 11 commits into from
9 changes: 7 additions & 2 deletions mypy/applytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from mypy.expandtype import expand_type
from mypy.types import Type, TypeVarId, TypeVarType, CallableType, AnyType, PartialType
from mypy.messages import MessageBuilder
from mypy.nodes import Context
from mypy.nodes import Context, CallExpr


def apply_generic_arguments(callable: CallableType, types: List[Type],
Expand Down Expand Up @@ -38,7 +38,12 @@ def apply_generic_arguments(callable: CallableType, types: List[Type],
types[i] = value
break
else:
msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context)
if isinstance(context, CallExpr):
msg.incompatible_typevar_value_in_call(
callable, callable.variables[i], context)
else:
msg.incompatible_typevar_value(
callable, type, callable.variables[i].name, context)
upper_bound = callable.variables[i].upper_bound
if (type and not isinstance(type, PartialType) and
not mypy.subtypes.is_subtype(type, upper_bound)):
Expand Down
57 changes: 48 additions & 9 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
from mypy.erasetype import erase_type
from mypy.errors import Errors
from mypy.types import (
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType,
Type, CallableType, Instance, TypeVarDef, TypeVarType, TupleType, TypedDictType,
UnionType, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
UninhabitedType, TypeOfAny
)
from mypy.nodes import (
TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_type_aliases,
ARG_POS, ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2,
ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT
ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, CallExpr
)


Expand Down Expand Up @@ -370,6 +370,14 @@ def format_distinctly(self, type1: Type, type2: Type, bare: bool = False) -> Tup
return (str1, str2)
return (str1, str2)

def format_arg_string(self, arg_type_str: str, arg_kind: int) -> str:
"""Given a type string and arg_kind, return a quoted, starred string."""
if arg_kind == ARG_STAR:
arg_type_str = '*' + arg_type_str
elif arg_kind == ARG_STAR2:
arg_type_str = '**' + arg_type_str
return self.quote_type_string(arg_type_str)

#
# Specific operations
#
Expand Down Expand Up @@ -613,12 +621,8 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
expected_type = callee.arg_types[-1]
arg_type_str, expected_type_str = self.format_distinctly(
arg_type, expected_type, bare=True)
if arg_kind == ARG_STAR:
arg_type_str = '*' + arg_type_str
elif arg_kind == ARG_STAR2:
arg_type_str = '**' + arg_type_str
msg = 'Argument {} {}has incompatible type {}; expected {}'.format(
n, target, self.quote_type_string(arg_type_str),
n, target, self.format_arg_string(arg_type_str, arg_kind),
self.quote_type_string(expected_type_str))
if isinstance(arg_type, Instance) and isinstance(expected_type, Instance):
notes = append_invariance_notes(notes, arg_type, expected_type)
Expand Down Expand Up @@ -884,6 +888,41 @@ def incompatible_typevar_value(self,
self.fail(INCOMPATIBLE_TYPEVAR_VALUE.format(typevar_name, callable_name(callee),
self.format(typ)), context)

def incompatible_typevar_value_in_call(self,
callee: CallableType,
variable: TypeVarDef,
context: CallExpr) -> None:
from mypy.typeanal import collect_all_inner_types
callee_name = callable_name(callee)
self.fail(
'Argument types for type variable "{}" are incompatible in call to {}'.format(
variable.name, callee_name),
context)
arg_names = []
for n, arg_type in enumerate(callee.arg_types):
# We collect all inner types to handle cases like List[T], where the
# argument itself is not the type variable.
for typ in [arg_type] + collect_all_inner_types(arg_type):
if isinstance(typ, TypeVarType) and typ.id == variable.id:
arg_names.append(
self.format_arg_string(callee.arg_names[n], callee.arg_kinds[n]))
break
valid_variable_types = ', '.join(
self.format(typ) for typ in variable.values)
if len(arg_names) > 1:
self.note(
'Arguments {} in call to {} must all have the same type for'
' "{}" (one of {})'.format(
format_string_list(arg_names, suppress=False),
callee_name, variable.name, valid_variable_types),
context)
else:
self.note(
'Argument {} in call to {} must have a valid type for "{}" (one'
' of {})'.format(
arg_names[0], callee_name, variable.name, valid_variable_types),
context)

def overloaded_signatures_overlap(self, index1: int, index2: int,
context: Context) -> None:
self.fail('Overloaded function signatures {} and {} overlap with '
Expand Down Expand Up @@ -1297,12 +1336,12 @@ def plural_s(s: Sequence[Any]) -> str:
return ''


def format_string_list(s: Iterable[str]) -> str:
def format_string_list(s: Iterable[str], suppress: bool = True) -> str:
lst = list(s)
assert len(lst) > 0
if len(lst) == 1:
return lst[0]
elif len(lst) <= 5:
elif not suppress or len(lst) <= 5:
return '%s and %s' % (', '.join(lst[:-1]), lst[-1])
else:
return '%s, ... and %s (%i methods suppressed)' % (
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,8 @@ fun1(1) # E: Argument 1 to "fun1" has incompatible type "int"; expected "List[Tu
fun1([(1, 'x')]) # E: Cannot infer type argument 1 of "fun1"

reveal_type(fun2([(1, 1)], 1)) # E: Revealed type is 'builtins.list[Tuple[builtins.int*, builtins.int*]]'
fun2([('x', 'x')], 'x') # E: Value of type variable "T" of "fun2" cannot be "str"
fun2([('x', 'x')], 'x') # E: Argument types for type variable "T" are incompatible in call to "fun2" \
Copy link
Member

Choose a reason for hiding this comment

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

Taking into account that positional arguments are more often used than keyword arguments, it is much more convenient to see the position of the argument, not its name at call site.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood; I'll look at making this switch as when I start passing the arguments through the call stack.

# N: Arguments "v" and "scale" in call to "fun2" must all have the same type for "T" (one of "int", "bool")
Copy link
Member

@ilevkivskyi ilevkivskyi Sep 3, 2017

Choose a reason for hiding this comment

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

In understand why you formulate the note this way (because of, e.g., T and List[T] argument types). But maybe you can show the note only if there are three or less constraints so that in this case you could propose valid variants, like:

note: Acceptable combinations of types for arguments 1 and 2 of "fun":
note:    "str" and "List[str]"
note:    "bytes" and "List[bytes]"
note: Got: "bytes" and "List[str]"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This feels like it might be an enhancement on top of this change (which should end up displaying what we expected to see, and what was actually passed in).


[builtins fixtures/list.pyi]

Expand Down
7 changes: 5 additions & 2 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -750,10 +750,13 @@ AnyStr = TypeVar('AnyStr', bytes, str)
def f(x: Union[AnyStr, int], *a: AnyStr) -> None: pass
f('foo')
f('foo', 'bar')
f('foo', b'bar') # E: Value of type variable "AnyStr" of "f" cannot be "object"
f('foo', b'bar') # E: Argument types for type variable "AnyStr" are incompatible in call to "f" \
# N: Arguments "x" and "*a" in call to "f" must all have the same type for "AnyStr" (one of "bytes", "str")
f(1)
f(1, 'foo')
f(1, 'foo', b'bar') # E: Value of type variable "AnyStr" of "f" cannot be "object"
f(1, 'foo', b'bar') # E: Argument types for type variable "AnyStr" are incompatible in call to "f" \
# N: Arguments "x" and "*a" in call to "f" must all have the same type for "AnyStr" (one of "bytes", "str")
f(1)
[builtins fixtures/primitives.pyi]


Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -998,10 +998,12 @@ def g(x: int, *a: AnyStr) -> None: pass

g('foo')
g('foo', 'bar')
g('foo', b'bar') # E: Value of type variable "AnyStr" of "g" cannot be "object"
g('foo', b'bar') # E: Argument types for type variable "AnyStr" are incompatible in call to "g" \
# N: Arguments "x" and "*a" in call to "g" must all have the same type for "AnyStr" (one of "bytes", "str")
g(1)
g(1, 'foo')
g(1, 'foo', b'bar') # E: Value of type variable "AnyStr" of "g" cannot be "object"
g(1, 'foo', b'bar') # E: Argument types for type variable "AnyStr" are incompatible in call to "g" \
# N: Argument "*a" in call to "g" must have a valid type for "AnyStr" (one of "bytes", "str")
[builtins fixtures/primitives.pyi]

[case testBadOverlapWithTypeVarsWithValues]
Expand Down
18 changes: 12 additions & 6 deletions test-data/unit/check-typevar-values.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ T = TypeVar('T', int, str)
def f(x: T) -> None: pass
f(1)
f('x')
f(object()) # E: Value of type variable "T" of "f" cannot be "object"
f(object()) # E: Argument types for type variable "T" are incompatible in call to "f" \
# N: Argument "x" in call to "f" must have a valid type for "T" (one of "int", "str")

[case testCallGenericFunctionWithTypeVarValueRestrictionUsingContext]
from typing import TypeVar, List
Expand All @@ -18,7 +19,8 @@ s = ['x']
o = [object()]
i = f(1)
s = f('')
o = f(1) # E: Value of type variable "T" of "f" cannot be "object"
o = f(1) # E: Argument types for type variable "T" are incompatible in call to "f" \
# N: Argument "x" in call to "f" must have a valid type for "T" (one of "int", "str")
[builtins fixtures/list.pyi]

[case testCallGenericFunctionWithTypeVarValueRestrictionAndAnyArgs]
Expand Down Expand Up @@ -239,7 +241,8 @@ class A(Generic[X]):
A(1)
A('x')
A(cast(Any, object()))
A(object()) # E: Value of type variable "X" of "A" cannot be "object"
A(object()) # E: Argument types for type variable "X" are incompatible in call to "A" \
# N: Argument "x" in call to "A" must have a valid type for "X" (one of "int", "str")

[case testGenericTypeWithTypevarValuesAndTypevarArgument]
from typing import TypeVar, Generic
Expand Down Expand Up @@ -402,8 +405,10 @@ Y = TypeVar('Y', int, str)
def f(x: X, y: Y) -> None: pass
f(A(), '')
f(B(), 1)
f(A(), A()) # E: Value of type variable "Y" of "f" cannot be "A"
f(1, 1) # E: Value of type variable "X" of "f" cannot be "int"
f(A(), A()) # E: Argument types for type variable "Y" are incompatible in call to "f" \
# N: Argument "y" in call to "f" must have a valid type for "Y" (one of "int", "str")
f(1, 1) # E: Argument types for type variable "X" are incompatible in call to "f" \
# N: Argument "x" in call to "f" must have a valid type for "X" (one of "A", "B")

[case testGenericFunctionWithNormalAndRestrictedTypevar]
from typing import TypeVar, Generic
Expand All @@ -418,7 +423,8 @@ def f(x: X, y: Y, z: int) -> None:
z = y # Error
y.foo # Error
[out]
main:8: error: Value of type variable "Y" of "C" cannot be "X"
main:8: error: Argument types for type variable "Y" are incompatible in call to "C"
main:8: note: Argument "y" in call to "C" must have a valid type for "Y" (one of "int", "str")
main:9: error: Incompatible types in assignment (expression has type "X", variable has type "int")
main:10: error: Incompatible types in assignment (expression has type "str", variable has type "int")
main:11: error: "int" has no attribute "foo"
Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,8 @@ re.subn(bpat, b'', b'')[0] + b''
re.subn(bre, lambda m: b'', b'')[0] + b''
re.subn(bpat, lambda m: b'', b'')[0] + b''
[out]
_program.py:7: error: Value of type variable "AnyStr" of "search" cannot be "object"
_program.py:7: error: Argument types for type variable "AnyStr" are incompatible in call to "search"
_program.py:7: note: Arguments "pattern" and "string" in call to "search" must all have the same type for "AnyStr" (one of "str", "bytes")
_program.py:9: error: Cannot infer type argument 1 of "search"

[case testReModuleString]
Expand All @@ -1301,7 +1302,8 @@ re.subn(spat, '', '')[0] + ''
re.subn(sre, lambda m: '', '')[0] + ''
re.subn(spat, lambda m: '', '')[0] + ''
[out]
_program.py:7: error: Value of type variable "AnyStr" of "search" cannot be "object"
_program.py:7: error: Argument types for type variable "AnyStr" are incompatible in call to "search"
_program.py:7: note: Arguments "pattern" and "string" in call to "search" must all have the same type for "AnyStr" (one of "str", "bytes")
_program.py:9: error: Cannot infer type argument 1 of "search"

[case testListSetitemTuple]
Expand Down