diff --git a/mypy/messages.py b/mypy/messages.py index 8dbf1dbeff95..6df38c2fc259 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -533,6 +533,7 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type: target = 'to {} '.format(name) msg = '' + notes = [] # type: List[str] if callee.name == '': name = callee.name[1:-1] n -= 1 @@ -571,7 +572,12 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type: arg_type_str = '**' + arg_type_str msg = 'Argument {} {}has incompatible type {}; expected {}'.format( n, target, arg_type_str, expected_type_str) + if isinstance(arg_type, Instance) and isinstance(expected_type, Instance): + notes = append_invariance_notes(notes, arg_type, expected_type) self.fail(msg, context) + if notes: + for note_msg in notes: + self.note(note_msg, context) def invalid_index_type(self, index_type: Type, expected_type: Type, base_str: str, context: Context) -> None: @@ -992,6 +998,33 @@ def pretty_or(args: List[str]) -> str: return ", ".join(quoted[:-1]) + ", or " + quoted[-1] +def append_invariance_notes(notes: List[str], arg_type: Instance, + expected_type: Instance) -> List[str]: + """Explain that the type is invariant and give notes for how to solve the issue.""" + from mypy.subtypes import is_subtype + from mypy.sametypes import is_same_type + invariant_type = '' + covariant_suggestion = '' + if (arg_type.type.fullname() == 'builtins.list' and + expected_type.type.fullname() == 'builtins.list' and + is_subtype(arg_type.args[0], expected_type.args[0])): + invariant_type = 'List' + covariant_suggestion = 'Consider using "Sequence" instead, which is covariant' + elif (arg_type.type.fullname() == 'builtins.dict' and + expected_type.type.fullname() == 'builtins.dict' and + is_same_type(arg_type.args[0], expected_type.args[0]) and + is_subtype(arg_type.args[1], expected_type.args[1])): + invariant_type = 'Dict' + covariant_suggestion = ('Consider using "Mapping" instead, ' + 'which is covariant in the value type') + if invariant_type and covariant_suggestion: + notes.append( + '"{}" is invariant -- see '.format(invariant_type) + + 'http://mypy.readthedocs.io/en/latest/common_issues.html#variance') + notes.append(covariant_suggestion) + return notes + + def make_inferred_type_note(context: Context, subtype: Type, supertype: Type, supertype_str: str) -> str: """Explain that the user may have forgotten to type a variable. diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 566841888693..d6b4cb99ee54 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -592,3 +592,48 @@ class C: def foo(self) -> None: pass C().foo() C().foo(1) # The decorator's return type says this should be okay + +[case testInvariantDictArgNote] +from typing import Dict, Sequence +def f(x: Dict[str, Sequence[int]]) -> None: pass +def g(x: Dict[str, float]) -> None: pass +def h(x: Dict[str, int]) -> None: pass +a = {'a': [1, 2]} +b = {'b': ['c', 'd']} +c = {'c': 1.0} +d = {'d': 1} +f(a) # E: Argument 1 to "f" has incompatible type Dict[str, List[int]]; expected Dict[str, Sequence[int]] \ + # N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \ + # N: Consider using "Mapping" instead, which is covariant in the value type +f(b) # E: Argument 1 to "f" has incompatible type Dict[str, List[str]]; expected Dict[str, Sequence[int]] +g(c) +g(d) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected Dict[str, float] \ + # N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \ + # N: Consider using "Mapping" instead, which is covariant in the value type +h(c) # E: Argument 1 to "h" has incompatible type Dict[str, float]; expected Dict[str, int] +h(d) +[builtins fixtures/dict.pyi] + +[case testInvariantListArgNote] +from typing import List, Union +def f(numbers: List[Union[int, float]]) -> None: pass +a = [1, 2] +f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected List[Union[int, float]] \ + # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \ + # N: Consider using "Sequence" instead, which is covariant +x = [1] +y = ['a'] +x = y # E: Incompatible types in assignment (expression has type List[str], variable has type List[int]) +[builtins fixtures/list.pyi] + +[case testInvariantTypeConfusingNames] +from typing import TypeVar +class Listener: pass +class DictReader: pass +def f(x: Listener) -> None: pass +def g(y: DictReader) -> None: pass +a = [1, 2] +b = {'b': 1} +f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected "Listener" +g(b) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected "DictReader" +[builtins fixtures/dict.pyi]