Skip to content

Commit

Permalink
Improve message when an annotation is expected (python#6782)
Browse files Browse the repository at this point in the history
Fixes python#2463

Improve error message when partial types are found for built-in types `Dict`, `List`, `Set`, `FrozenSet`.

Messages used to be like:
```
main:3: error: Need type annotation for 'x'
```
This PR modifies it to look like:
```
main:3: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
```

In case the variable is not of a built-in type the hints are not shown.
  • Loading branch information
rafaelcaricio authored and ilevkivskyi committed May 6, 2019
1 parent c4a6e40 commit a202b42
Show file tree
Hide file tree
Showing 14 changed files with 74 additions and 49 deletions.
9 changes: 5 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2496,13 +2496,13 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue,
# partial type which will be made more specific later. A partial type
# gets generated in assignment like 'x = []' where item type is not known.
if not self.infer_partial_type(name, lvalue, init_type):
self.msg.need_annotation_for_var(name, context)
self.msg.need_annotation_for_var(name, context, self.options.python_version)
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
elif (isinstance(lvalue, MemberExpr) and self.inferred_attribute_types is not None
and lvalue.def_var and lvalue.def_var in self.inferred_attribute_types
and not is_same_type(self.inferred_attribute_types[lvalue.def_var], init_type)):
# Multiple, inconsistent types inferred for an attribute.
self.msg.need_annotation_for_var(name, context)
self.msg.need_annotation_for_var(name, context, self.options.python_version)
name.type = AnyType(TypeOfAny.from_error)
else:
# Infer type of the target.
Expand Down Expand Up @@ -3724,7 +3724,7 @@ def enter_partial_types(self, *, is_function: bool = False,
var.type = NoneType()
else:
if var not in self.partial_reported and not permissive:
self.msg.need_annotation_for_var(var, context)
self.msg.need_annotation_for_var(var, context, self.options.python_version)
self.partial_reported.add(var)
if var.type:
var.type = self.fixup_partial_type(var.type)
Expand All @@ -3748,7 +3748,8 @@ def handle_partial_var_type(
if in_scope:
context = partial_types[node]
if is_local or not self.options.allow_untyped_globals:
self.msg.need_annotation_for_var(node, context)
self.msg.need_annotation_for_var(node, context,
self.options.python_version)
else:
# Defer the node -- we might get a better type in the outer scope
self.handle_cannot_determine_type(node.name(), context)
Expand Down
21 changes: 18 additions & 3 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
from mypy.types import (
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType,
UnionType, NoneType, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
UninhabitedType, TypeOfAny, ForwardRef, UnboundType
UninhabitedType, TypeOfAny, ForwardRef, UnboundType, PartialType
)
from mypy.nodes import (
TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_builtin_aliases,
ARG_POS, ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2,
ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, SymbolNode,
CallExpr
)
from mypy.defaults import PYTHON3_VERSION
from mypy.util import unmangle
from mypy import message_registry

Expand Down Expand Up @@ -1077,8 +1078,22 @@ def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> N
self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)),
ctx)

def need_annotation_for_var(self, node: SymbolNode, context: Context) -> None:
self.fail("Need type annotation for '{}'".format(unmangle(node.name())), context)
def need_annotation_for_var(self, node: SymbolNode, context: Context,
python_version: Optional[Tuple[int, int]] = None) -> None:
hint = ''
# Only gives hint if it's a variable declaration and the partial type is a builtin type
if (python_version and isinstance(node, Var) and isinstance(node.type, PartialType) and
node.type.type and node.type.type.fullname() in reverse_builtin_aliases):
alias = reverse_builtin_aliases[node.type.type.fullname()]
alias = alias.split('.')[-1]
type_dec = '<type>'
if alias == 'Dict':
type_dec = '{}, {}'.format(type_dec, type_dec)
if python_version < (3, 6):
hint = ' (hint: "{} = ... # type: {}[{}]")'.format(node.name(), alias, type_dec)
else:
hint = ' (hint: "{}: {}[{}] = ...")'.format(node.name(), alias, type_dec)
self.fail("Need type annotation for '{}'{}".format(unmangle(node.name()), hint), context)

def explicit_any(self, ctx: Context) -> None:
self.fail('Explicit "Any" is not allowed', ctx)
Expand Down
6 changes: 4 additions & 2 deletions mypy/test/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,14 @@ def expand_errors(input: List[str], output: List[str], fnam: str) -> None:
elif m.group(1) == 'W':
severity = 'warning'
col = m.group('col')
message = m.group('message')
message = message.replace('\\#', '#') # adds back escaped # character
if col is None:
output.append(
'{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message')))
'{}:{}: {}: {}'.format(fnam, i + 1, severity, message))
else:
output.append('{}:{}:{}: {}: {}'.format(
fnam, i + 1, col, severity, m.group('message')))
fnam, i + 1, col, severity, message))


def fix_win_path(line: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
STUBGEN_PY]

# We split the pytest run into three parts to improve test
# parallelization. Each run should have tests that each take a roughly similiar
# parallelization. Each run should have tests that each take a roughly similar
# time to run.
cmds = {
# Self type check
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Add the test in this format anywhere in the file:
with text "abc..."
- note a space after `E:` and `flags:`
- `# E:12` adds column number to the expected error
- use `\` to escape the `#` character and indicate that the rest of the line is part of
the error message
- repeating `# E: ` several times in one line indicates multiple expected errors in one line
- `W: ...` and `N: ...` works exactly like `E:`, but report a warning and a note respectively
- lines that don't contain the above should cause no type check errors
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ class C:
x = C.x
[builtins fixtures/list.pyi]
[out]
main:2: error: Need type annotation for 'x'
main:2: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")

[case testAccessingGenericClassAttribute]
from typing import Generic, TypeVar
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1714,7 +1714,7 @@ d5 = dict(a=1, b='') # type: Dict[str, Any]

[case testDictWithoutKeywordArgs]
from typing import Dict
d = dict() # E: Need type annotation for 'd'
d = dict() # E: Need type annotation for 'd' (hint: "d: Dict[<type>, <type>] = ...")
d2 = dict() # type: Dict[int, str]
dict(undefined) # E: Name 'undefined' is not defined
[builtins fixtures/dict.pyi]
Expand Down Expand Up @@ -1953,8 +1953,8 @@ a.__pow__() # E: Too few arguments for "__pow__" of "int"
[builtins fixtures/ops.pyi]

[case testTypeAnnotationNeededMultipleAssignment]
x, y = [], [] # E: Need type annotation for 'x' \
# E: Need type annotation for 'y'
x, y = [], [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...") \
# E: Need type annotation for 'y' (hint: "y: List[<type>] = ...")
[builtins fixtures/list.pyi]

[case testStrictEqualityEq]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@ if g(C()):
def f(x: B) -> B: pass

[case testRedefineFunctionDefinedAsVariableInitializedToEmptyList]
f = [] # E: Need type annotation for 'f'
f = [] # E: Need type annotation for 'f' (hint: "f: List[<type>] = ...")
if object():
def f(): pass # E: Incompatible redefinition
f() # E: "List[Any]" not callable
Expand Down Expand Up @@ -2320,7 +2320,7 @@ def make_list() -> List[T]: pass

l: List[int] = make_list()

bad = make_list() # E: Need type annotation for 'bad'
bad = make_list() # E: Need type annotation for 'bad' (hint: "bad: List[<type>] = ...")
[builtins fixtures/list.pyi]

[case testAnonymousArgumentError]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-inference-context.test
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ class B(A): pass
[case testLocalVariableInferenceFromEmptyList]
import typing
def f() -> None:
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
b = [None]
c = [B()]
if int():
Expand Down
50 changes: 25 additions & 25 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -1375,20 +1375,20 @@ a.append(0) # E: Argument 1 to "append" of "list" has incompatible type "int";
[out]

[case testInferListInitializedToEmptyAndNotAnnotated]
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
[builtins fixtures/list.pyi]
[out]

[case testInferListInitializedToEmptyAndReadBeforeAppend]
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
if a: pass
a.xyz # E: "List[Any]" has no attribute "xyz"
a.append('')
[builtins fixtures/list.pyi]
[out]

[case testInferListInitializedToEmptyAndIncompleteTypeInAppend]
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
a.append([])
a() # E: "List[Any]" not callable
[builtins fixtures/list.pyi]
Expand All @@ -1413,7 +1413,7 @@ def f() -> None:

[case testInferListInitializedToEmptyAndNotAnnotatedInFunction]
def f() -> None:
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")

def g() -> None: pass

Expand All @@ -1424,7 +1424,7 @@ a.append(1)

[case testInferListInitializedToEmptyAndReadBeforeAppendInFunction]
def f() -> None:
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
if a: pass
a.xyz # E: "List[Any]" has no attribute "xyz"
a.append('')
Expand All @@ -1441,7 +1441,7 @@ class A:

[case testInferListInitializedToEmptyAndNotAnnotatedInClassBody]
class A:
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")

class B:
a = []
Expand All @@ -1461,15 +1461,15 @@ class A:
[case testInferListInitializedToEmptyAndNotAnnotatedInMethod]
class A:
def f(self) -> None:
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
[builtins fixtures/list.pyi]
[out]

[case testInferListInitializedToEmptyInMethodViaAttribute]
class A:
def f(self) -> None:
# Attributes aren't supported right now.
self.a = [] # E: Need type annotation for 'a'
self.a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
self.a.append(1)
self.a.append('')
[builtins fixtures/list.pyi]
Expand All @@ -1480,7 +1480,7 @@ from typing import List

class A:
def __init__(self) -> None:
self.x = [] # E: Need type annotation for 'x'
self.x = [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")

class B(A):
# TODO?: This error is kind of a false positive, unfortunately
Expand Down Expand Up @@ -1526,16 +1526,16 @@ a() # E: "Dict[str, int]" not callable
[out]

[case testInferDictInitializedToEmptyUsingUpdateError]
a = {} # E: Need type annotation for 'a'
a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
a.update([1, 2]) # E: Argument 1 to "update" of "dict" has incompatible type "List[int]"; expected "Mapping[Any, Any]"
a() # E: "Dict[Any, Any]" not callable
[builtins fixtures/dict.pyi]
[out]

[case testInferDictInitializedToEmptyAndIncompleteTypeInUpdate]
a = {} # E: Need type annotation for 'a'
a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
a[1] = {}
b = {} # E: Need type annotation for 'b'
b = {} # E: Need type annotation for 'b' (hint: "b: Dict[<type>, <type>] = ...")
b[{}] = 1
[builtins fixtures/dict.pyi]
[out]
Expand All @@ -1555,14 +1555,14 @@ def add():
[case testSpecialCaseEmptyListInitialization]
def f(blocks: Any): # E: Name 'Any' is not defined \
# N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
to_process = [] # E: Need type annotation for 'to_process'
to_process = [] # E: Need type annotation for 'to_process' (hint: "to_process: List[<type>] = ...")
to_process = list(blocks)
[builtins fixtures/list.pyi]
[out]

[case testSpecialCaseEmptyListInitialization2]
def f(blocks: object):
to_process = [] # E: Need type annotation for 'to_process'
to_process = [] # E: Need type annotation for 'to_process' (hint: "to_process: List[<type>] = ...")
to_process = list(blocks) # E: No overload variant of "list" matches argument type "object" \
# N: Possible overload variant: \
# N: def [T] __init__(self, x: Iterable[T]) -> List[T] \
Expand Down Expand Up @@ -1621,7 +1621,7 @@ x.append('') # E: Argument 1 to "append" of "list" has incompatible type "str";
x = None
if object():
# Promote from partial None to partial list.
x = [] # E: Need type annotation for 'x'
x = [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
x
[builtins fixtures/list.pyi]

Expand All @@ -1630,7 +1630,7 @@ def f() -> None:
x = None
if object():
# Promote from partial None to partial list.
x = [] # E: Need type annotation for 'x'
x = [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
[builtins fixtures/list.pyi]
[out]

Expand Down Expand Up @@ -1716,7 +1716,7 @@ class A:
pass
[builtins fixtures/for.pyi]
[out]
main:3: error: Need type annotation for 'x'
main:3: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")

[case testPartialTypeErrorSpecialCase3]
class A:
Expand Down Expand Up @@ -1882,9 +1882,9 @@ o = 1

[case testMultipassAndPartialTypesSpecialCase3]
def f() -> None:
x = {} # E: Need type annotation for 'x'
x = {} # E: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
y = o
z = {} # E: Need type annotation for 'z'
z = {} # E: Need type annotation for 'z' (hint: "z: Dict[<type>, <type>] = ...")
o = 1
[builtins fixtures/dict.pyi]
[out]
Expand Down Expand Up @@ -2070,7 +2070,7 @@ main:4: error: Invalid type: try using Literal[0] instead?
class C:
x = None
def __init__(self) -> None:
self.x = [] # E: Need type annotation for 'x'
self.x = [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
[builtins fixtures/list.pyi]
[out]

Expand Down Expand Up @@ -2246,7 +2246,7 @@ class A:
[case testLocalPartialTypesWithClassAttributeInitializedToEmptyDict]
# flags: --local-partial-types
class A:
x = {} # E: Need type annotation for 'x'
x = {} # E: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")

def f(self) -> None:
self.x[0] = ''
Expand All @@ -2269,7 +2269,7 @@ reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]'

[case testLocalPartialTypesWithGlobalInitializedToEmptyList2]
# flags: --local-partial-types
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")

def f() -> None:
a.append(1)
Expand All @@ -2280,7 +2280,7 @@ reveal_type(a) # E: Revealed type is 'builtins.list[Any]'

[case testLocalPartialTypesWithGlobalInitializedToEmptyList3]
# flags: --local-partial-types
a = [] # E: Need type annotation for 'a'
a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")

def f():
a.append(1)
Expand All @@ -2302,7 +2302,7 @@ reveal_type(a) # E: Revealed type is 'builtins.dict[builtins.int, builtins.str]'

[case testLocalPartialTypesWithGlobalInitializedToEmptyDict2]
# flags: --local-partial-types
a = {} # E: Need type annotation for 'a'
a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")

def f() -> None:
a[0] = ''
Expand All @@ -2313,7 +2313,7 @@ reveal_type(a) # E: Revealed type is 'builtins.dict[Any, Any]'

[case testLocalPartialTypesWithGlobalInitializedToEmptyDict3]
# flags: --local-partial-types
a = {} # E: Need type annotation for 'a'
a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")

def f():
a[0] = ''
Expand Down
5 changes: 5 additions & 0 deletions test-data/unit/check-python2.test
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,8 @@ def foo(
[case testNoneHasNoBoolInPython2]
none = None
b = none.__bool__() # E: "None" has no attribute "__bool__"

[case testDictWithoutTypeCommentInPython2]
# flags: --py2
d = dict() # E: Need type annotation for 'd' (hint: "d = ... \# type: Dict[<type>, <type>]")
[builtins_py2 fixtures/floatdict_python2.pyi]
4 changes: 2 additions & 2 deletions test-data/unit/check-tuples.test
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,8 @@ d, e = f, g, h = 1, 1 # E: Need more than 2 values to unpack (3 expected)
[case testAssignmentToStarMissingAnnotation]
from typing import List
t = 1, 2
a, b, *c = 1, 2 # E: Need type annotation for 'c'
aa, bb, *cc = t # E: Need type annotation for 'cc'
a, b, *c = 1, 2 # E: Need type annotation for 'c' (hint: "c: List[<type>] = ...")
aa, bb, *cc = t # E: Need type annotation for 'cc' (hint: "cc: List[<type>] = ...")
[builtins fixtures/list.pyi]

[case testAssignmentToStarAnnotation]
Expand Down
Loading

0 comments on commit a202b42

Please sign in to comment.