From a202b421bb85f55219f0beaa7d969978b1d3dcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Car=C3=ADcio?= Date: Mon, 6 May 2019 17:29:26 -0400 Subject: [PATCH] Improve message when an annotation is expected (#6782) Fixes #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[] = ...") ``` In case the variable is not of a built-in type the hints are not shown. --- mypy/checker.py | 9 ++-- mypy/messages.py | 21 +++++++-- mypy/test/data.py | 6 ++- runtests.py | 2 +- test-data/unit/README.md | 2 + test-data/unit/check-classes.test | 2 +- test-data/unit/check-expressions.test | 6 +-- test-data/unit/check-functions.test | 4 +- test-data/unit/check-inference-context.test | 2 +- test-data/unit/check-inference.test | 50 ++++++++++----------- test-data/unit/check-python2.test | 5 +++ test-data/unit/check-tuples.test | 4 +- test-data/unit/cmdline.test | 2 +- test-data/unit/fine-grained.test | 8 ++-- 14 files changed, 74 insertions(+), 49 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2af33533a25f..f07900909217 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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. @@ -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) @@ -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) diff --git a/mypy/messages.py b/mypy/messages.py index 87f1cd7e0917..ce4e6330e596 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -21,7 +21,7 @@ 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, @@ -29,6 +29,7 @@ ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, SymbolNode, CallExpr ) +from mypy.defaults import PYTHON3_VERSION from mypy.util import unmangle from mypy import message_registry @@ -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 = '' + 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) diff --git a/mypy/test/data.py b/mypy/test/data.py index 2c6780fdd26e..165edb6bccd6 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -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: diff --git a/runtests.py b/runtests.py index 36c2dfa2a014..ade078fe60c3 100755 --- a/runtests.py +++ b/runtests.py @@ -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 diff --git a/test-data/unit/README.md b/test-data/unit/README.md index 3c95a3e0f67e..7454126fe570 100644 --- a/test-data/unit/README.md +++ b/test-data/unit/README.md @@ -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 diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index f40373c30211..4c0c5fa2651a 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -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[] = ...") [case testAccessingGenericClassAttribute] from typing import Generic, TypeVar diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index e8a9b0ee8b39..8d50a04fe7da 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -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[, ] = ...") d2 = dict() # type: Dict[int, str] dict(undefined) # E: Name 'undefined' is not defined [builtins fixtures/dict.pyi] @@ -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[] = ...") \ + # E: Need type annotation for 'y' (hint: "y: List[] = ...") [builtins fixtures/list.pyi] [case testStrictEqualityEq] diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index c3d6c08fe6c5..11c7376de452 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -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[] = ...") if object(): def f(): pass # E: Incompatible redefinition f() # E: "List[Any]" not callable @@ -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[] = ...") [builtins fixtures/list.pyi] [case testAnonymousArgumentError] diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index d075d439cce6..af3abab0ce23 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -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[] = ...") b = [None] c = [B()] if int(): diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index e7d59525c5bc..656c1b67f5ad 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -1375,12 +1375,12 @@ 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[] = ...") [builtins fixtures/list.pyi] [out] [case testInferListInitializedToEmptyAndReadBeforeAppend] -a = [] # E: Need type annotation for 'a' +a = [] # E: Need type annotation for 'a' (hint: "a: List[] = ...") if a: pass a.xyz # E: "List[Any]" has no attribute "xyz" a.append('') @@ -1388,7 +1388,7 @@ a.append('') [out] [case testInferListInitializedToEmptyAndIncompleteTypeInAppend] -a = [] # E: Need type annotation for 'a' +a = [] # E: Need type annotation for 'a' (hint: "a: List[] = ...") a.append([]) a() # E: "List[Any]" not callable [builtins fixtures/list.pyi] @@ -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[] = ...") def g() -> None: pass @@ -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[] = ...") if a: pass a.xyz # E: "List[Any]" has no attribute "xyz" a.append('') @@ -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[] = ...") class B: a = [] @@ -1461,7 +1461,7 @@ 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[] = ...") [builtins fixtures/list.pyi] [out] @@ -1469,7 +1469,7 @@ class A: 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[] = ...") self.a.append(1) self.a.append('') [builtins fixtures/list.pyi] @@ -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[] = ...") class B(A): # TODO?: This error is kind of a false positive, unfortunately @@ -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[, ] = ...") 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[, ] = ...") a[1] = {} -b = {} # E: Need type annotation for 'b' +b = {} # E: Need type annotation for 'b' (hint: "b: Dict[, ] = ...") b[{}] = 1 [builtins fixtures/dict.pyi] [out] @@ -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[] = ...") 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[] = ...") 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] \ @@ -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[] = ...") x [builtins fixtures/list.pyi] @@ -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[] = ...") [builtins fixtures/list.pyi] [out] @@ -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[] = ...") [case testPartialTypeErrorSpecialCase3] class A: @@ -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[, ] = ...") y = o - z = {} # E: Need type annotation for 'z' + z = {} # E: Need type annotation for 'z' (hint: "z: Dict[, ] = ...") o = 1 [builtins fixtures/dict.pyi] [out] @@ -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[] = ...") [builtins fixtures/list.pyi] [out] @@ -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[, ] = ...") def f(self) -> None: self.x[0] = '' @@ -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[] = ...") def f() -> None: a.append(1) @@ -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[] = ...") def f(): a.append(1) @@ -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[, ] = ...") def f() -> None: a[0] = '' @@ -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[, ] = ...") def f(): a[0] = '' diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test index f3bc0481ead2..fcec1f1d78cc 100644 --- a/test-data/unit/check-python2.test +++ b/test-data/unit/check-python2.test @@ -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[, ]") +[builtins_py2 fixtures/floatdict_python2.pyi] diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index c0dace5fe2b6..8246372a58fe 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -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[] = ...") +aa, bb, *cc = t # E: Need type annotation for 'cc' (hint: "cc: List[] = ...") [builtins fixtures/list.pyi] [case testAssignmentToStarAnnotation] diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index dad7d32b59f1..dcb822459135 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -924,7 +924,7 @@ y: List = [] # error m.py:3: error: Missing type parameters for generic type m.py:5: error: Missing type parameters for generic type m.py:6: error: Missing type parameters for generic type -m.py:8: error: Need type annotation for 'x' +m.py:8: error: Need type annotation for 'x' (hint: "x: List[] = ...") m.py:9: error: Missing type parameters for generic type [case testDisallowAnyGenericsCustomGenericClass] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 5ca900c84a21..78622142da44 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2699,9 +2699,9 @@ def g() -> None: pass def g() -> int: pass [builtins fixtures/dict.pyi] [out] -main:7: error: Need type annotation for 'x' +main:7: error: Need type annotation for 'x' (hint: "x: Dict[, ] = ...") == -main:7: error: Need type annotation for 'x' +main:7: error: Need type annotation for 'x' (hint: "x: Dict[, ] = ...") [case testRefreshPartialTypeInClass] import a @@ -2716,9 +2716,9 @@ def g() -> None: pass def g() -> int: pass [builtins fixtures/dict.pyi] [out] -main:5: error: Need type annotation for 'x' +main:5: error: Need type annotation for 'x' (hint: "x: Dict[, ] = ...") == -main:5: error: Need type annotation for 'x' +main:5: error: Need type annotation for 'x' (hint: "x: Dict[, ] = ...") [case testRefreshTryExcept] import a