Skip to content

Commit d1e597d

Browse files
authored
Allow type ignores after type comments (#6591)
This is a hack in fastparse, but allows for the following to pass typechecking: `x = 1 # type: str # type: ignore` This also handles the edge case where there is a `# type: ignore` in a comment, which we don't want to pick up. See the tests for more examples. Fixes #5967
1 parent affb032 commit d1e597d

File tree

3 files changed

+88
-30
lines changed

3 files changed

+88
-30
lines changed

mypy/fastparse.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import sys
23

34
from typing import (
@@ -120,6 +121,8 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str,
120121

121122
TYPE_COMMENT_SYNTAX_ERROR = 'syntax error in type comment' # type: Final
122123

124+
TYPE_IGNORE_PATTERN = re.compile(r'[^#]*#\s*type:\s*ignore\s*($|#)')
125+
123126

124127
# Older versions of typing don't allow using overload outside stubs,
125128
# so provide a dummy.
@@ -180,18 +183,19 @@ def parse_type_comment(type_comment: str,
180183
line: int,
181184
errors: Optional[Errors],
182185
assume_str_is_unicode: bool = True,
183-
) -> Optional[Type]:
186+
) -> Tuple[bool, Optional[Type]]:
184187
try:
185188
typ = ast3_parse(type_comment, '<type_comment>', 'eval')
186189
except SyntaxError as e:
187190
if errors is not None:
188191
errors.report(line, e.offset, TYPE_COMMENT_SYNTAX_ERROR, blocker=True)
189-
return None
192+
return False, None
190193
else:
191194
raise
192195
else:
196+
extra_ignore = TYPE_IGNORE_PATTERN.match(type_comment) is not None
193197
assert isinstance(typ, ast3_Expression)
194-
return TypeConverter(errors, line=line,
198+
return extra_ignore, TypeConverter(errors, line=line,
195199
assume_str_is_unicode=assume_str_is_unicode).visit(typ.body)
196200

197201

@@ -212,8 +216,8 @@ def parse_type_string(expr_string: str, expr_fallback_name: str,
212216
code with unicode_literals...) and setting `assume_str_is_unicode` accordingly.
213217
"""
214218
try:
215-
node = parse_type_comment(expr_string.strip(), line=line, errors=None,
216-
assume_str_is_unicode=assume_str_is_unicode)
219+
_, node = parse_type_comment(expr_string.strip(), line=line, errors=None,
220+
assume_str_is_unicode=assume_str_is_unicode)
217221
if isinstance(node, UnboundType) and node.original_str_expr is None:
218222
node.original_str_expr = expr_string
219223
node.original_str_fallback = expr_fallback_name
@@ -247,6 +251,8 @@ def __init__(self,
247251
self.is_stub = is_stub
248252
self.errors = errors
249253

254+
self.extra_type_ignores = [] # type: List[int]
255+
250256
# Cache of visit_X methods keyed by type of visited object
251257
self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]]
252258

@@ -389,11 +395,12 @@ def translate_module_id(self, id: str) -> str:
389395

390396
def visit_Module(self, mod: ast3.Module) -> MypyFile:
391397
body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
392-
398+
ignores = [ti.lineno for ti in mod.type_ignores]
399+
ignores.extend(self.extra_type_ignores)
393400
return MypyFile(body,
394401
self.imports,
395402
False,
396-
{ti.lineno for ti in mod.type_ignores},
403+
{*ignores},
397404
)
398405

399406
# --- stmt ---
@@ -587,7 +594,10 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int,
587594
if annotation is not None:
588595
arg_type = TypeConverter(self.errors, line=arg.lineno).visit(annotation)
589596
elif type_comment is not None:
590-
arg_type = parse_type_comment(type_comment, arg.lineno, self.errors)
597+
extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, self.errors)
598+
if extra_ignore:
599+
self.extra_type_ignores.append(arg.lineno)
600+
591601
return Argument(Var(arg.arg), arg_type, self.visit(default), kind)
592602

593603
def fail_arg(self, msg: str, arg: ast3.arg) -> None:
@@ -642,7 +652,9 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt:
642652
lvalues = self.translate_expr_list(n.targets)
643653
rvalue = self.visit(n.value)
644654
if n.type_comment is not None:
645-
typ = parse_type_comment(n.type_comment, n.lineno, self.errors)
655+
extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors)
656+
if extra_ignore:
657+
self.extra_type_ignores.append(n.lineno)
646658
else:
647659
typ = None
648660
s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False)
@@ -674,7 +686,9 @@ def visit_NamedExpr(self, n: NamedExpr) -> None:
674686
# For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
675687
def visit_For(self, n: ast3.For) -> ForStmt:
676688
if n.type_comment is not None:
677-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
689+
extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
690+
if extra_ignore:
691+
self.extra_type_ignores.append(n.lineno)
678692
else:
679693
target_type = None
680694
node = ForStmt(self.visit(n.target),
@@ -687,7 +701,9 @@ def visit_For(self, n: ast3.For) -> ForStmt:
687701
# AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
688702
def visit_AsyncFor(self, n: ast3.AsyncFor) -> ForStmt:
689703
if n.type_comment is not None:
690-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
704+
extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
705+
if extra_ignore:
706+
self.extra_type_ignores.append(n.lineno)
691707
else:
692708
target_type = None
693709
node = ForStmt(self.visit(n.target),
@@ -716,7 +732,9 @@ def visit_If(self, n: ast3.If) -> IfStmt:
716732
# With(withitem* items, stmt* body, string? type_comment)
717733
def visit_With(self, n: ast3.With) -> WithStmt:
718734
if n.type_comment is not None:
719-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
735+
extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
736+
if extra_ignore:
737+
self.extra_type_ignores.append(n.lineno)
720738
else:
721739
target_type = None
722740
node = WithStmt([self.visit(i.context_expr) for i in n.items],
@@ -728,7 +746,9 @@ def visit_With(self, n: ast3.With) -> WithStmt:
728746
# AsyncWith(withitem* items, stmt* body, string? type_comment)
729747
def visit_AsyncWith(self, n: ast3.AsyncWith) -> WithStmt:
730748
if n.type_comment is not None:
731-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
749+
extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, self.errors)
750+
if extra_ignore:
751+
self.extra_type_ignores.append(n.lineno)
732752
else:
733753
target_type = None
734754
s = WithStmt([self.visit(i.context_expr) for i in n.items],
@@ -1211,11 +1231,11 @@ def visit_raw_str(self, s: str) -> Type:
12111231
# An escape hatch that allows the AST walker in fastparse2 to
12121232
# directly hook into the Python 3.5 type converter in some cases
12131233
# without needing to create an intermediary `Str` object.
1214-
return (parse_type_comment(s.strip(),
1215-
self.line,
1216-
self.errors,
1217-
self.assume_str_is_unicode)
1218-
or AnyType(TypeOfAny.from_error))
1234+
_, typ = parse_type_comment(s.strip(),
1235+
self.line,
1236+
self.errors,
1237+
self.assume_str_is_unicode)
1238+
return typ or AnyType(TypeOfAny.from_error)
12191239

12201240
def visit_Call(self, e: Call) -> Type:
12211241
# Parse the arg constructor

mypy/fastparse2.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def __init__(self,
162162
# Cache of visit_X methods keyed by type of visited object
163163
self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]]
164164

165+
self.extra_type_ignores = [] # type: List[int]
166+
165167
def fail(self, msg: str, line: int, column: int) -> None:
166168
self.errors.report(line, column, msg, blocker=True)
167169

@@ -301,11 +303,12 @@ def translate_module_id(self, id: str) -> str:
301303

302304
def visit_Module(self, mod: ast27.Module) -> MypyFile:
303305
body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
304-
306+
ignores = [ti.lineno for ti in mod.type_ignores]
307+
ignores.extend(self.extra_type_ignores)
305308
return MypyFile(body,
306309
self.imports,
307310
False,
308-
{ti.lineno for ti in mod.type_ignores},
311+
{*ignores},
309312
)
310313

311314
# --- stmt ---
@@ -543,8 +546,10 @@ def visit_Delete(self, n: ast27.Delete) -> DelStmt:
543546
def visit_Assign(self, n: ast27.Assign) -> AssignmentStmt:
544547
typ = None
545548
if n.type_comment:
546-
typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
547-
assume_str_is_unicode=self.unicode_literals)
549+
extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
550+
assume_str_is_unicode=self.unicode_literals)
551+
if extra_ignore:
552+
self.extra_type_ignores.append(n.lineno)
548553

549554
stmt = AssignmentStmt(self.translate_expr_list(n.targets),
550555
self.visit(n.value),
@@ -561,15 +566,17 @@ def visit_AugAssign(self, n: ast27.AugAssign) -> OperatorAssignmentStmt:
561566
# For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
562567
def visit_For(self, n: ast27.For) -> ForStmt:
563568
if n.type_comment is not None:
564-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors,
565-
assume_str_is_unicode=self.unicode_literals)
569+
extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
570+
assume_str_is_unicode=self.unicode_literals)
571+
if extra_ignore:
572+
self.extra_type_ignores.append(n.lineno)
566573
else:
567-
target_type = None
574+
typ = None
568575
stmt = ForStmt(self.visit(n.target),
569576
self.visit(n.iter),
570577
self.as_required_block(n.body, n.lineno),
571578
self.as_block(n.orelse, n.lineno),
572-
target_type)
579+
typ)
573580
return self.set_line(stmt, n)
574581

575582
# While(expr test, stmt* body, stmt* orelse)
@@ -589,14 +596,16 @@ def visit_If(self, n: ast27.If) -> IfStmt:
589596
# With(withitem* items, stmt* body, string? type_comment)
590597
def visit_With(self, n: ast27.With) -> WithStmt:
591598
if n.type_comment is not None:
592-
target_type = parse_type_comment(n.type_comment, n.lineno, self.errors,
593-
assume_str_is_unicode=self.unicode_literals)
599+
extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors,
600+
assume_str_is_unicode=self.unicode_literals)
601+
if extra_ignore:
602+
self.extra_type_ignores.append(n.lineno)
594603
else:
595-
target_type = None
604+
typ = None
596605
stmt = WithStmt([self.visit(n.context_expr)],
597606
[self.visit(n.optional_vars)],
598607
self.as_required_block(n.body, n.lineno),
599-
target_type)
608+
typ)
600609
return self.set_line(stmt, n)
601610

602611
def visit_Raise(self, n: ast27.Raise) -> RaiseStmt:

test-data/unit/check-fastparse.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,35 @@ def f4(x: Iterable[x][x]) -> None: pass # E: Invalid type comment or annotation
8888
def f5(x: Callable[..., int][x]) -> None: pass # E: Invalid type comment or annotation
8989
def f6(x: Callable[..., int].x) -> None: pass # E: Invalid type comment or annotation
9090

91+
[case testFastParseTypeWithIgnore]
92+
def f(x, # type: x # type: ignore
93+
):
94+
# type: (...) -> None
95+
pass
96+
97+
[case testFastParseVariableTypeWithIgnore]
98+
99+
x = 1 # type: str # type: ignore
100+
101+
[case testFastParseVariableTypeWithIgnoreNoSpace]
102+
103+
x = 1 # type: str #type:ignore
104+
105+
[case testFastParseVariableTypeWithIgnoreAndComment]
106+
107+
x = 1 # type: str # type: ignore # comment
108+
109+
[case testFastParseTypeWithIgnoreWithStmt]
110+
with open('test', 'r') as f: # type: int # type: ignore
111+
pass
112+
113+
[case testFastParseTypeWithIgnoreForStmt]
114+
for i in (1, 2, 3, 100): # type: str # type: ignore
115+
pass
116+
117+
[case testFastParseVariableCommentThenIgnore]
118+
a="test" # type: int #comment # type: ignore # E: Incompatible types in assignment (expression has type "str", variable has type "int")
119+
91120
[case testFastParseProperty]
92121

93122
class C:

0 commit comments

Comments
 (0)