Repository: python/mypy

Issue:
Add import hints for errors like 'Any' undefined
If new users forget to import `Any`, `List` or similar, it could be useful to add a hint such as this:

```
x.py:20: error: Name "Any" undefined
x.py:20: note: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
```

We could do this for some common stdlib functionality as well, such as `collections`, `subprocess`, `re`, etc.

Even better, mypy could also give import hints for some user-defined classes and functions (that have unique definitions).


Discussion:
Maybe also if they misspell it as `any`.
I'm taking a look at this.
The top five imported names are:
* `Optional`
* `Tuple`
* `Union`
* `Callable`
* `Any`

Resolving patch:
diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py
--- a/mypy/newsemanal/semanal.py
+++ b/mypy/newsemanal/semanal.py
@@ -142,6 +142,21 @@
     'builtins.classmethod': 'classmethod.pyi',
 }  # type: Final
 
+TYPES_FOR_UNIMPORTED_HINTS = {
+    'typing.Any',
+    'typing.Callable',
+    'typing.Dict',
+    'typing.Iterable',
+    'typing.Iterator',
+    'typing.List',
+    'typing.Optional',
+    'typing.Set',
+    'typing.Tuple',
+    'typing.TypeVar',
+    'typing.Union',
+    'typing.cast',
+}  # type: Final
+
 
 # Special cased built-in classes that are needed for basic functionality and need to be
 # available very early on.
@@ -3821,9 +3836,14 @@ def lookup(self, name: str, ctx: Context,
         return None
 
     def check_for_obsolete_short_name(self, name: str, ctx: Context) -> None:
-        matches = [obsolete_name
-                   for obsolete_name in obsolete_name_mapping
-                   if obsolete_name.rsplit('.', 1)[-1] == name]
+        lowercased_names_handled_by_unimported_hints = {
+            name.lower() for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        matches = [
+            obsolete_name for obsolete_name in obsolete_name_mapping
+            if obsolete_name.rsplit('.', 1)[-1] == name
+            and obsolete_name not in lowercased_names_handled_by_unimported_hints
+        ]
         if len(matches) == 1:
             self.note("(Did you mean '{}'?)".format(obsolete_name_mapping[matches[0]]), ctx)
 
@@ -4296,6 +4316,7 @@ def name_not_defined(self, name: str, ctx: Context, namespace: Optional[str] = N
             message += ' {}'.format(extra)
         self.fail(message, ctx)
         self.check_for_obsolete_short_name(name, ctx)
+
         if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES:
             # The user probably has a missing definition in a test fixture. Let's verify.
             fullname = 'builtins.{}'.format(name)
@@ -4303,6 +4324,25 @@ def name_not_defined(self, name: str, ctx: Context, namespace: Optional[str] = N
                 # Yes. Generate a helpful note.
                 self.add_fixture_note(fullname, ctx)
 
+        modules_with_unimported_hints = {
+            name.split('.', 1)[0]
+            for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        lowercased = {
+            name.lower(): name
+            for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        for module in modules_with_unimported_hints:
+            fullname = '{}.{}'.format(module, name).lower()
+            if fullname not in lowercased:
+                continue
+            # User probably forgot to import these types.
+            hint = (
+                'Did you forget to import it from "{module}"?'
+                ' (Suggestion: "from {module} import {name}")'
+            ).format(module=module, name=lowercased[fullname].rsplit('.', 1)[-1])
+            self.note(hint, ctx)
+
     def already_defined(self, name: str, ctx: Context,
                         original_ctx: Optional[Union[SymbolTableNode, SymbolNode]] = None, *,
                         noun: str) -> None:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -166,6 +166,21 @@
     'builtins.classmethod': 'classmethod.pyi',
 }  # type: Final
 
+TYPES_FOR_UNIMPORTED_HINTS = {
+    'typing.Any',
+    'typing.Callable',
+    'typing.Dict',
+    'typing.Iterable',
+    'typing.Iterator',
+    'typing.List',
+    'typing.Optional',
+    'typing.Set',
+    'typing.Tuple',
+    'typing.TypeVar',
+    'typing.Union',
+    'typing.cast',
+}  # type: Final
+
 
 class SemanticAnalyzerPass2(NodeVisitor[None],
                             SemanticAnalyzerInterface,
@@ -3462,9 +3477,14 @@ def lookup(self, name: str, ctx: Context,
         return None
 
     def check_for_obsolete_short_name(self, name: str, ctx: Context) -> None:
-        matches = [obsolete_name
-                   for obsolete_name in obsolete_name_mapping
-                   if obsolete_name.rsplit('.', 1)[-1] == name]
+        lowercased_names_handled_by_unimported_hints = {
+            name.lower() for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        matches = [
+            obsolete_name for obsolete_name in obsolete_name_mapping
+            if obsolete_name.rsplit('.', 1)[-1] == name
+            and obsolete_name not in lowercased_names_handled_by_unimported_hints
+        ]
         if len(matches) == 1:
             self.note("(Did you mean '{}'?)".format(obsolete_name_mapping[matches[0]]), ctx)
 
@@ -3734,6 +3754,7 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
         if extra:
             message += ' {}'.format(extra)
         self.fail(message, ctx)
+
         if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES:
             # The user probably has a missing definition in a test fixture. Let's verify.
             fullname = 'builtins.{}'.format(name)
@@ -3741,6 +3762,25 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
                 # Yes. Generate a helpful note.
                 self.add_fixture_note(fullname, ctx)
 
+        modules_with_unimported_hints = {
+            name.split('.', 1)[0]
+            for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        lowercased = {
+            name.lower(): name
+            for name in TYPES_FOR_UNIMPORTED_HINTS
+        }
+        for module in modules_with_unimported_hints:
+            fullname = '{}.{}'.format(module, name).lower()
+            if fullname not in lowercased:
+                continue
+            # User probably forgot to import these types.
+            hint = (
+                'Did you forget to import it from "{module}"?'
+                ' (Suggestion: "from {module} import {name}")'
+            ).format(module=module, name=lowercased[fullname].rsplit('.', 1)[-1])
+            self.note(hint, ctx)
+
     def already_defined(self, name: str, ctx: Context,
                         original_ctx: Optional[Union[SymbolTableNode, SymbolNode]] = None, *,
                         noun: str) -> None:


Test patch:
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -2206,3 +2206,21 @@ C in exp  # E: Non-overlapping container check (element type: "Type[C]", contain
 o in exp
 [builtins fixtures/list.pyi]
 [typing fixtures/typing-full.pyi]
+
+
+[case testUnimportedHintAny]
+def f(x: Any) -> None:  # E: Name 'Any' is not defined \
+                        # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
+    pass
+
+
+[case testUnimportedHintAnyLower]
+def f(x: any) -> None:  # E: Name 'any' is not defined \
+                        # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
+    pass
+
+
+[case testUnimportedHintOptional]
+def f(x: Optional[str]) -> None:  # E: Name 'Optional' is not defined \
+                                  # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Optional")
+    pass
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -1553,7 +1553,8 @@ def add():
 [builtins fixtures/dict.pyi]
 
 [case testSpecialCaseEmptyListInitialization]
-def f(blocks: Any): # E: Name 'Any' is not defined
+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 = list(blocks)
 [builtins fixtures/list.pyi]
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -7841,11 +7841,13 @@ def bar(x: T) -> T:
 x = 1
 [out]
 a.py:1: error: Name 'TypeVar' is not defined
+a.py:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import TypeVar")
 a.py:7: error: Invalid type "a.T"
 a.py:10: error: Name 'bar' already defined on line 6
 a.py:11: error: Invalid type "a.T"
 ==
 a.py:1: error: Name 'TypeVar' is not defined
+a.py:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import TypeVar")
 a.py:7: error: Invalid type "a.T"
 a.py:10: error: Name 'bar' already defined on line 6
 a.py:11: error: Invalid type "a.T"
diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test
--- a/test-data/unit/pythoneval.test
+++ b/test-data/unit/pythoneval.test
@@ -242,6 +242,7 @@ import typing
 cast(int, 2)
 [out]
 _program.py:2: error: Name 'cast' is not defined
+_program.py:2: note: Did you forget to import it from "typing"? (Suggestion: "from typing import cast")
 
 [case testBinaryIOType]
 from typing import BinaryIO
diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test
--- a/test-data/unit/semanal-errors.test
+++ b/test-data/unit/semanal-errors.test
@@ -1220,6 +1220,7 @@ def f() -> List[int]: pass
 [builtins fixtures/list.pyi]
 [out]
 main:2: error: Name 'List' is not defined
+main:2: note: Did you forget to import it from "typing"? (Suggestion: "from typing import List")
 
 [case testImportObsoleteTypingFunction]
 from typing import Function # E: Module 'typing' has no attribute 'Function' (it's now called 'typing.Callable')
@@ -1260,7 +1261,7 @@ main:1: error: Module 'typing' has no attribute 'typevar' (it's now called 'typi
 t = typevar('t')
 [out]
 main:1: error: Name 'typevar' is not defined
-main:1: note: (Did you mean 'typing.TypeVar'?)
+main:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import TypeVar")
 
 [case testUseObsoleteNameForTypeVar3]
 import typing