Skip to content

Commit 2534523

Browse files
quartoxilevkivskyi
authored andcommitted
Add informative notes to invariant function arguments (#3411)
Fixes #1115 and #3352 by providing a link to docs and a suggestion. This currently only takes care of 'List' and 'Dict', which are the most typical for this kind of errors.
1 parent a1b2e6a commit 2534523

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed

mypy/messages.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
542542
target = 'to {} '.format(name)
543543

544544
msg = ''
545+
notes = [] # type: List[str]
545546
if callee.name == '<list>':
546547
name = callee.name[1:-1]
547548
n -= 1
@@ -610,7 +611,12 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
610611
arg_type_str = '**' + arg_type_str
611612
msg = 'Argument {} {}has incompatible type {}; expected {}'.format(
612613
n, target, arg_type_str, expected_type_str)
614+
if isinstance(arg_type, Instance) and isinstance(expected_type, Instance):
615+
notes = append_invariance_notes(notes, arg_type, expected_type)
613616
self.fail(msg, context)
617+
if notes:
618+
for note_msg in notes:
619+
self.note(note_msg, context)
614620

615621
def invalid_index_type(self, index_type: Type, expected_type: Type, base_str: str,
616622
context: Context) -> None:
@@ -1345,6 +1351,33 @@ def pretty_or(args: List[str]) -> str:
13451351
return ", ".join(quoted[:-1]) + ", or " + quoted[-1]
13461352

13471353

1354+
def append_invariance_notes(notes: List[str], arg_type: Instance,
1355+
expected_type: Instance) -> List[str]:
1356+
"""Explain that the type is invariant and give notes for how to solve the issue."""
1357+
from mypy.subtypes import is_subtype
1358+
from mypy.sametypes import is_same_type
1359+
invariant_type = ''
1360+
covariant_suggestion = ''
1361+
if (arg_type.type.fullname() == 'builtins.list' and
1362+
expected_type.type.fullname() == 'builtins.list' and
1363+
is_subtype(arg_type.args[0], expected_type.args[0])):
1364+
invariant_type = 'List'
1365+
covariant_suggestion = 'Consider using "Sequence" instead, which is covariant'
1366+
elif (arg_type.type.fullname() == 'builtins.dict' and
1367+
expected_type.type.fullname() == 'builtins.dict' and
1368+
is_same_type(arg_type.args[0], expected_type.args[0]) and
1369+
is_subtype(arg_type.args[1], expected_type.args[1])):
1370+
invariant_type = 'Dict'
1371+
covariant_suggestion = ('Consider using "Mapping" instead, '
1372+
'which is covariant in the value type')
1373+
if invariant_type and covariant_suggestion:
1374+
notes.append(
1375+
'"{}" is invariant -- see '.format(invariant_type) +
1376+
'http://mypy.readthedocs.io/en/latest/common_issues.html#variance')
1377+
notes.append(covariant_suggestion)
1378+
return notes
1379+
1380+
13481381
def make_inferred_type_note(context: Context, subtype: Type,
13491382
supertype: Type, supertype_str: str) -> str:
13501383
"""Explain that the user may have forgotten to type a variable.

test-data/unit/check-varargs.test

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,3 +593,48 @@ class C:
593593
def foo(self) -> None: pass
594594
C().foo()
595595
C().foo(1) # The decorator's return type says this should be okay
596+
597+
[case testInvariantDictArgNote]
598+
from typing import Dict, Sequence
599+
def f(x: Dict[str, Sequence[int]]) -> None: pass
600+
def g(x: Dict[str, float]) -> None: pass
601+
def h(x: Dict[str, int]) -> None: pass
602+
a = {'a': [1, 2]}
603+
b = {'b': ['c', 'd']}
604+
c = {'c': 1.0}
605+
d = {'d': 1}
606+
f(a) # E: Argument 1 to "f" has incompatible type Dict[str, List[int]]; expected Dict[str, Sequence[int]] \
607+
# N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
608+
# N: Consider using "Mapping" instead, which is covariant in the value type
609+
f(b) # E: Argument 1 to "f" has incompatible type Dict[str, List[str]]; expected Dict[str, Sequence[int]]
610+
g(c)
611+
g(d) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected Dict[str, float] \
612+
# N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
613+
# N: Consider using "Mapping" instead, which is covariant in the value type
614+
h(c) # E: Argument 1 to "h" has incompatible type Dict[str, float]; expected Dict[str, int]
615+
h(d)
616+
[builtins fixtures/dict.pyi]
617+
618+
[case testInvariantListArgNote]
619+
from typing import List, Union
620+
def f(numbers: List[Union[int, float]]) -> None: pass
621+
a = [1, 2]
622+
f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected List[Union[int, float]] \
623+
# N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
624+
# N: Consider using "Sequence" instead, which is covariant
625+
x = [1]
626+
y = ['a']
627+
x = y # E: Incompatible types in assignment (expression has type List[str], variable has type List[int])
628+
[builtins fixtures/list.pyi]
629+
630+
[case testInvariantTypeConfusingNames]
631+
from typing import TypeVar
632+
class Listener: pass
633+
class DictReader: pass
634+
def f(x: Listener) -> None: pass
635+
def g(y: DictReader) -> None: pass
636+
a = [1, 2]
637+
b = {'b': 1}
638+
f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected "Listener"
639+
g(b) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected "DictReader"
640+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)