Repository: python/mypy
Pull request: Ignoring a file with a single # type: ignore comment.
Fixes #626. For background on the design choices, here is what our current ASTs allow us to find:

- In all versions of Python, we can accurately detect bare `# type: ignore` comments before the first statement in a module.
- In Python 3.8+, we can accurately detect the placement of bare `# type: ignore` comments within a list of statements (we need `end_lineno` to do this properly).
- *In no current version of Python* can we accurately detect bare `# type: ignore` comments before the first statement in an indented code block using the AST alone.

Considering these limitations, the behavior I chose here is:

- All versions of Python will ignore a whole file if it contains a bare `# type: ignore` comment *before its first statement.*
- ~~Python 3.8+ will ignore all statements following a bare `# type: ignore` comment *at the top level of a module.*~~ This is now a separate PR: #6841.
- **Bare `# type: ignore` comments have no special meaning inside of indented code blocks.** If we were to allow this, we would need to do one of the following (and I personally don't like any of them):
  - Not recognize them at the start of a block. 🙁
  - Guess where the `for`/`while`/`with`/`class`/`def`/`try` bit ends, a la decorated definitions prior to Python 3.8. 🙁
  - Parse the raw source of the file ourselves to see where the above block-starters end. 🙁
  - Modify ast parsing to tell us where the block-starters end. 🙁

A few notes:

- Ignoring is accomplished by nesting all remaining statements in an unreachable `Block`.
- `ASTConverter.extra_type_ignores: List[int]` is now `ASTConverter.type_ignores: Set[int]` and is populated before the AST is converted.
- Tests are added in `check-ignore.test` ~~and `check-38.test`~~.

Problem statement:
Allow using # type: ignore to skip type checking in a file
As per PEP 484 discussion, it should be possible to use `# type: ignore` to skip type checking in the rest of a file:

```
# type: ignore
... # ignore type errors here
```

Currently the comment can only be used to ignore errors on individual lines.

Hints from issue discussion:
(Moved assignment and Milestone from the duplicate #964.)

I wonder what we should do if this happens within a function. Should we then ignore all errors within the function only? Example:

```
def f() -> None:
    # type: ignore
    return 1 + 'x'   # don't report an error?

a = 1 + 'y'   # report an error here?
```

This could be nice, as it would let people annotate functions but ignore errors in the body of the function. `@no_type_check` is not a good match, as it also causes the annotations to be ignored.

Yeah, I agree that this should be block-scoped.

Hello,

Alternatively, could there be a way to exclude certain files or directories from command line or in the configuration file?

Currently, if I type `mypy my_directory`, the entire folder is scanned. It would be very handy to be able to add an `--exclude` argument.

In my case, I want to exclude automatically created Django migration files, but still scan my entire Django app.
You can use the `ignore_errors` per-file option.

Thank you @gvanrossum, this seems to be what I was looking for.

Could you please assist me a bit further? I tried to follow [the instructions](http://mypy.readthedocs.io/en/latest/config_file.html), but something is still not working:

**mypy.ini**
```
[mypy]

[gw_box.migrations.*]
ignore_errors = True
```

```console
$ mypy gw_box --ignore-missing-imports --config-file mypy.ini
gw_box/migrations/0001_initial.py:14: error: Need type annotation for variable
```
You need to add a section like this:
```
[mypy-dropbox.edgestore.datatypes.namespace]
ignore_errors = False
```
For docs see http://mypy.readthedocs.io/en/latest/config_file.html
Thank you, I figured out I was just missing the `mypy-` part.

P.S. Just figured out who you were. Huge thank you for creating Python :)
@ddfisher Are you planning to work on this?
I would love to see this feature get implemented.  In my tests I'm deliberately trying to break things, so being able to ignore the whole file is very useful.  I'm currently using mypy.ini, but I'd much rather prefer adding a comment at the top of the file.

Commit message: Appease mypy.

Patch to review:
diff --git a/mypy/fastparse.py b/mypy/fastparse.py
index 9d91601d5347..cc4c7e6cafaa 100644
--- a/mypy/fastparse.py
+++ b/mypy/fastparse.py
@@ -2,7 +2,7 @@
 import sys
 
 from typing import (
-    Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload
+    Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload, Set
 )
 MYPY = False
 if MYPY:
@@ -258,7 +258,7 @@ def __init__(self,
         self.is_stub = is_stub
         self.errors = errors
 
-        self.extra_type_ignores = []  # type: List[int]
+        self.type_ignores = set()  # type: Set[int]
 
         # Cache of visit_X methods keyed by type of visited object
         self.visitor_cache = {}  # type: Dict[type, Callable[[Optional[AST]], Any]]
@@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
+            b.is_unreachable = True
+            return [b]
+
         res = []  # type: List[Statement]
-        for e in l:
+        line = 0
+
+        for i, e in enumerate(l):
+
+            if module:  # and...
+                if sys.version_info >= (3, 8):
+
+                    # In Python 3.8, a "# type: ignore" comment between statements at
+                    # the top level of a module skips checking for everything else:
+
+                    ignores = set(range(line + 1, self.get_line(e))) & self.type_ignores
+
+                    if ignores:
+                        self.errors.used_ignored_lines[self.errors.file].add(min(ignores))
+                        b = Block(self.fix_function_overloads(self.translate_stmt_list(l[i:])))
+                        b.is_unreachable = True
+                        res.append(b)
+                        return res
+
+                    line = e.end_lineno if e.end_lineno is not None else e.lineno
+
             stmt = self.visit(e)
             res.append(stmt)
+
         return res
 
     op_map = {
@@ -403,13 +442,12 @@ def translate_module_id(self, id: str) -> str:
         return id
 
     def visit_Module(self, mod: ast3.Module) -> MypyFile:
-        body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
-        ignores = [ti.lineno for ti in mod.type_ignores]
-        ignores.extend(self.extra_type_ignores)
+        self.type_ignores = {ti.lineno for ti in mod.type_ignores}
+        body = self.fix_function_overloads(self.translate_stmt_list(mod.body, module=True))
         return MypyFile(body,
                         self.imports,
                         False,
-                        set(ignores),
+                        set(self.type_ignores),
                         )
 
     # --- stmt ---
@@ -615,7 +653,7 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int,
             elif type_comment is not None:
                 extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, self.errors)
                 if extra_ignore:
-                    self.extra_type_ignores.append(arg.lineno)
+                    self.type_ignores.add(arg.lineno)
 
         return Argument(Var(arg.arg), arg_type, self.visit(default), kind)
 
@@ -673,7 +711,7 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt:
         if n.type_comment is not None:
             extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             typ = None
         s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False)
@@ -707,7 +745,7 @@ def visit_For(self, n: ast3.For) -> ForStmt:
         if n.type_comment is not None:
             extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             target_type = None
         node = ForStmt(self.visit(n.target),
@@ -722,7 +760,7 @@ def visit_AsyncFor(self, n: ast3.AsyncFor) -> ForStmt:
         if n.type_comment is not None:
             extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             target_type = None
         node = ForStmt(self.visit(n.target),
@@ -753,7 +791,7 @@ def visit_With(self, n: ast3.With) -> WithStmt:
         if n.type_comment is not None:
             extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             target_type = None
         node = WithStmt([self.visit(i.context_expr) for i in n.items],
@@ -767,7 +805,7 @@ def visit_AsyncWith(self, n: ast3.AsyncWith) -> WithStmt:
         if n.type_comment is not None:
             extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             target_type = None
         s = WithStmt([self.visit(i.context_expr) for i in n.items],
diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py
index b4b831aa9e74..ae9d1694dfd5 100644
--- a/mypy/fastparse2.py
+++ b/mypy/fastparse2.py
@@ -16,7 +16,7 @@
 """
 import sys
 
-from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List
+from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, Set
 MYPY = False
 if MYPY:
     import typing  # for typing.Type, which conflicts with types.Type
@@ -163,7 +163,7 @@ def __init__(self,
         # Cache of visit_X methods keyed by type of visited object
         self.visitor_cache = {}  # type: Dict[type, Callable[[Optional[AST]], Any]]
 
-        self.extra_type_ignores = []  # type: List[int]
+        self.type_ignores = set()  # type: Set[int]
 
     def fail(self, msg: str, line: int, column: int, blocker: bool = True) -> None:
         if blocker or not self.options.ignore_errors:
@@ -193,7 +193,24 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast27.stmt) -> int:
+        if isinstance(n, (ast27.ClassDef, ast27.FunctionDef)) and n.decorator_list:
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast27.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
+            b.is_unreachable = True
+            return [b]
+
         res = []  # type: List[Statement]
         for e in l:
             stmt = self.visit(e)
@@ -304,13 +321,12 @@ def translate_module_id(self, id: str) -> str:
         return id
 
     def visit_Module(self, mod: ast27.Module) -> MypyFile:
+        self.type_ignores = {ti.lineno for ti in mod.type_ignores}
         body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
-        ignores = [ti.lineno for ti in mod.type_ignores]
-        ignores.extend(self.extra_type_ignores)
         return MypyFile(body,
                         self.imports,
                         False,
-                        set(ignores),
+                        set(self.type_ignores),
                         )
 
     # --- stmt ---
@@ -558,7 +574,7 @@ def visit_Assign(self, n: ast27.Assign) -> AssignmentStmt:
             extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
                                                    assume_str_is_unicode=self.unicode_literals)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
 
         stmt = AssignmentStmt(self.translate_expr_list(n.targets),
                               self.visit(n.value),
@@ -578,7 +594,7 @@ def visit_For(self, n: ast27.For) -> ForStmt:
             extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
                                                    assume_str_is_unicode=self.unicode_literals)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             typ = None
         stmt = ForStmt(self.visit(n.target),
@@ -608,7 +624,7 @@ def visit_With(self, n: ast27.With) -> WithStmt:
             extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
                                                    assume_str_is_unicode=self.unicode_literals)
             if extra_ignore:
-                self.extra_type_ignores.append(n.lineno)
+                self.type_ignores.add(n.lineno)
         else:
             typ = None
         stmt = WithStmt([self.visit(n.context_expr)],
diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test
index 16f279545bc8..376fea612386 100644
--- a/test-data/unit/check-38.test
+++ b/test-data/unit/check-38.test
@@ -106,3 +106,8 @@ def g(x: int): ...
     /
     0  # type: ignore
 )  # type: ignore  # E: unused 'type: ignore' comment
+
+[case testIgnoreRestOfModule]
+ERROR  # E: Name 'ERROR' is not defined
+# type: ignore
+IGNORE
diff --git a/test-data/unit/check-ignore.test b/test-data/unit/check-ignore.test
index c17befa6e0f4..3992819a4648 100644
--- a/test-data/unit/check-ignore.test
+++ b/test-data/unit/check-ignore.test
@@ -218,3 +218,43 @@ def f() -> None: pass
 
 [case testCannotIgnoreBlockingError]
 yield  # type: ignore  # E: 'yield' outside function
+
+[case testIgnoreWholeModule1]
+# flags: --warn-unused-ignores
+# type: ignore
+IGNORE # type: ignore  # E: unused 'type: ignore' comment
+
+[case testIgnoreWholeModule2]
+# type: ignore
+if True:
+    IGNORE
+
+[case testIgnoreWholeModule3]
+# type: ignore
+@d
+class C: ...
+IGNORE
+
+[case testIgnoreWholeModule4]
+# type: ignore
+@d
+
+def f(): ...
+IGNORE
+
+[case testDontIgnoreWholeModule1]
+if True:
+    # type: ignore
+    ERROR # E: Name 'ERROR' is not defined
+ERROR # E: Name 'ERROR' is not defined
+
+[case testDontIgnoreWholeModule2]
+@d  # type: ignore
+class C: ...
+ERROR # E: Name 'ERROR' is not defined
+
+[case testDontIgnoreWholeModule3]
+@d  # type: ignore
+
+def f(): ...
+ERROR # E: Name 'ERROR' is not defined


Code review comments:
diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
path: mypy/fastparse.py
text: @user1:
Please choose a longer name for this argument, maybe `stmts`? (I know the original code uses one-letter names, but I don't want to encourage the practice for new code, and you've added a lot here.)

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
+            b.is_unreachable = True
+            return [b]
+
         res = []  # type: List[Statement]
-        for e in l:
+        line = 0
+
+        for i, e in enumerate(l):
+
+            if module:  # and...
+                if sys.version_info >= (3, 8):
+
+                    # In Python 3.8, a "# type: ignore" comment between statements at
+                    # the top level of a module skips checking for everything else:
path: mypy/fastparse.py
text: @user1:
Most importantly, explain why: this check needs access to `end_lineno` which only exists in 3.8 and up.

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
path: mypy/fastparse.py
text: @user1:
Maybe rename to `get_lineno`? Without context, the name `get_line` vaguely makes me think that it would get the source code line somehow.

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
path: mypy/fastparse.py
text: @user1:
Perhaps rename to `ismodule`?

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
+            b.is_unreachable = True
+            return [b]
+
         res = []  # type: List[Statement]
-        for e in l:
+        line = 0
+
+        for i, e in enumerate(l):
+
+            if module:  # and...
path: mypy/fastparse.py
text: @user1:
Somehow it looks inefficient to check for `module` and `sys.version_info` each time through the loop. Also the comment here adds nothing. You did know `and` short-circuits, right? You can combine the two checks and save one level of indentation.

@author:
It was originally `if module and sys.version_info >= (3, 8):`, but the mypy self check complained when I accessed `e.end_lineno` on 336. As I understand it, the mypy's version checks don't work for compound tests like this? Splitting it up silences the error, and I believe the emitted bytecode is identical.

If it would be clearer, I can just `# type: ignore` line 336 instead!

@user1:
OK, then the two `if` statements is optimal -- just put in a clearer comment explaining that this is to work around a mypy issue.

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
+            b.is_unreachable = True
+            return [b]
+
         res = []  # type: List[Statement]
-        for e in l:
+        line = 0
+
+        for i, e in enumerate(l):
path: mypy/fastparse.py
text: @user1:
Again, I'd propose a longer name for `e`, e.g. `stmt`. (But, `i` for the index is traditional, so please keep that.)

diff_hunk: @@ -294,11 +294,50 @@ def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]:
             res.append(exp)
         return res
 
-    def translate_stmt_list(self, l: Sequence[AST]) -> List[Statement]:
+    def get_line(self, n: ast3.stmt) -> int:
+        if (isinstance(n, (ast3.AsyncFunctionDef, ast3.ClassDef, ast3.FunctionDef))
+                and n.decorator_list):
+            return n.decorator_list[0].lineno
+        return n.lineno
+
+    def translate_stmt_list(self,
+                            l: Sequence[ast3.stmt],
+                            module: bool = False) -> List[Statement]:
+
+        # A "# type: ignore" comment before the first statement of a module
+        # ignores the whole module:
+
+        if module and l and self.type_ignores and min(self.type_ignores) < self.get_line(l[0]):
+            self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores))
+            b = Block(self.fix_function_overloads(self.translate_stmt_list(l)))
path: mypy/fastparse.py
text: @user1:
Maybe rename to `block`?