Skip to content

Added parsing of ignore annotation for import statements. #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions mypy/annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Classes for representing mypy annotations"""

from typing import List

import mypy.nodes


class Annotation(mypy.nodes.Context):
"""Abstract base class for all annotations."""

def __init__(self, line: int = -1) -> None:
self.line = line


class IgnoreAnnotation(Annotation):
"""The 'mypy: ignore' annotation"""

def __init__(self, line: int = -1) -> None:
super().__init__(line)

def get_line(self) -> int:
return self.line
38 changes: 34 additions & 4 deletions mypy/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from mypy.parsetype import (
parse_type, parse_types, parse_signature, TypeParseError
)
from mypy.annotations import Annotation, IgnoreAnnotation
from mypy.parseannotation import parse_annotation, AnnotationParseError


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing empty line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see that I'm not the only one who cares about theses things 😄

precedence = {
Expand Down Expand Up @@ -162,8 +164,10 @@ def parse_import(self) -> Import:
break
commas.append(self.expect(','))
br = self.expect_break()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also support this for from m import x at least? Maybe the imported names such as x should be defined and have type Any. Or alternatively, we could require all code that refers to them to use if not MYPY: but that would be less convenient.

annotation = self.parse_annotation_comment(br)
node = Import(ids)
self.imports.append(node)
if not isinstance(annotation, IgnoreAnnotation):
self.imports.append(node)
self.set_repr(node, noderepr.ImportRepr(import_tok, id_toks, as_names,
commas, br))
return node
Expand Down Expand Up @@ -208,7 +212,9 @@ def parse_import_from(self) -> Node:
if node is None:
node = ImportFrom(name, targets)
br = self.expect_break()
self.imports.append(node)
annotation = self.parse_annotation_comment(br)
if not isinstance(annotation, IgnoreAnnotation):
self.imports.append(node)
# TODO: Fix representation if there is a custom typing module import.
self.set_repr(node, noderepr.ImportFromRepr(
from_tok, components, import_tok, lparen, name_toks, rparen, br))
Expand Down Expand Up @@ -1683,7 +1689,7 @@ def parse_type(self) -> Type:
raise ParseError()
return typ

annotation_prefix_re = re.compile(r'#\s*type:')
type_annotation_prefix_re = re.compile(r'#\s*type:')

def parse_type_comment(self, token: Token, signature: bool) -> Type:
"""Parse a '# type: ...' annotation.
Expand All @@ -1692,7 +1698,7 @@ def parse_type_comment(self, token: Token, signature: bool) -> Type:
a type signature of form (...) -> t.
"""
whitespace_or_comments = token.rep().strip()
if self.annotation_prefix_re.match(whitespace_or_comments):
if self.type_annotation_prefix_re.match(whitespace_or_comments):
type_as_str = whitespace_or_comments.split(':', 1)[1].strip()
tokens = lex.lex(type_as_str, token.line)
if len(tokens) < 2:
Expand All @@ -1714,6 +1720,30 @@ def parse_type_comment(self, token: Token, signature: bool) -> Type:
else:
return None

annotation_prefix_re = re.compile(r'#\s*mypy:')

def parse_annotation_comment(self, token: Token) -> Annotation:
"""Parse a '# mypy: ...' annotation"""
whitespace_or_comments = token.rep().strip()
if self.annotation_prefix_re.match(whitespace_or_comments):
annotation_as_str = whitespace_or_comments.split(':', 1)[1].strip()
tokens = lex.lex(annotation_as_str, token.line)
if len(tokens) < 2:
# Empty annotation (only Eof token)
self.errors.report(token.line, 'Empty annotation')
return None
try:
annotation, index = parse_annotation(tokens, 0)
except AnnotationParseError as e:
self.parse_error_at(e.token, skip = False)
return None
if index < len(tokens) - 2:
self.parse_error_at(tokens[index], skip=False)
return None
return annotation
else:
return None

# Representation management

def set_repr(self, node: Node, repr: Any) -> None:
Expand Down
51 changes: 51 additions & 0 deletions mypy/parseannotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Annotation parse"""

from typing import List, Tuple

from mypy.lex import Token
from mypy import nodes
from mypy.annotations import Annotation, IgnoreAnnotation


class AnnotationParseError(Exception):
def __init__(self, token: Token, index: int) -> None:
super().__init__()
self.token = token
self.index = index


def parse_annotation(tok: List[Token], index: int) -> Tuple[Annotation, int]:
"""Parse an annotation
"""

p = AnnotationParser(tok, index)
return p.parse_annotation(), p.index()

class AnnotationParser:
def __init__(self, tok: List[Token], ind: int) -> None:
self.tok = tok
self.ind = ind

def index(self) -> int:
return self.ind

def parse_annotation(self) -> Annotation:
"""Parse an annotation."""
t = self.current_token()
if t.string == 'ignore':
self.skip()
return IgnoreAnnotation(t.line)
else:
self.parse_error()

# Helpers:

def skip(self) -> Token:
self.ind += 1
return self.tok[self.ind - 1]

def current_token(self) -> Token:
return self.tok[self.ind]

def parse_error(self) -> None:
raise AnnotationParseError(self.tok[self.ind], self.ind)
10 changes: 10 additions & 0 deletions mypy/test/data/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,16 @@ xyz()
[out]
main, line 1: No module named 'xyz'

[case testAccessingUnknownModuleIgnore]
import xyz # mypy: ignore
xyz.foo()
[out]

[case AccessingNameImportedFromUnknownModuleIgnore]
from xyz import abc # mypy: ignore
abc()
[out]

[case testAccessingUnknownModule2]
import xyz, bar
xyz.foo()
Expand Down
17 changes: 17 additions & 0 deletions mypy/test/data/parse-annotation.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Test cases for annotation parser.

[case testIgnoreAnnotation]
import xyz # mypy: ignore
[out]
MypyFile:1(
Import:1(xyz : xyz))

[case testEmptyAnnotation]
import xyz # mypy:
[out]
<input>, line 1: Empty annotation

[case testInvalidAnnotation]
import xyz # mypy: xxx
[out]
<input>, line 1: Parse error before "xxx"
3 changes: 2 additions & 1 deletion mypy/test/testparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

class ParserSuite(Suite):
parse_files = ['parse.test',
'parse-python2.test']
'parse-python2.test',
'parse-annotation.test']

def cases(self):
# The test case descriptions are stored in data files.
Expand Down