From 7494984672af5f8d8fbc4406a36276120fbbe00e Mon Sep 17 00:00:00 2001 From: Sander Kersten Date: Sat, 29 Nov 2014 12:56:34 +0100 Subject: [PATCH 1/4] Added parsing of ignore annotation for import statements. --- mypy/annotations.py | 22 +++++++++++++ mypy/parse.py | 35 ++++++++++++++++++--- mypy/parseannotation.py | 51 +++++++++++++++++++++++++++++++ mypy/test/data/check-modules.test | 4 +++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 mypy/annotations.py create mode 100644 mypy/parseannotation.py diff --git a/mypy/annotations.py b/mypy/annotations.py new file mode 100644 index 000000000000..f7363032606b --- /dev/null +++ b/mypy/annotations.py @@ -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 diff --git a/mypy/parse.py b/mypy/parse.py index 534ad7968e81..3cddda66a1e9 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -33,7 +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 precedence = { '**': 16, @@ -162,8 +163,10 @@ def parse_import(self) -> Import: break commas.append(self.expect(',')) br = self.expect_break() + 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 @@ -1683,7 +1686,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. @@ -1692,7 +1695,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: @@ -1714,6 +1717,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 TypeParseError 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: diff --git a/mypy/parseannotation.py b/mypy/parseannotation.py new file mode 100644 index 000000000000..2eb7df67fcf5 --- /dev/null +++ b/mypy/parseannotation.py @@ -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) \ No newline at end of file diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test index b021a44f1ced..92c65023bbbc 100644 --- a/mypy/test/data/check-modules.test +++ b/mypy/test/data/check-modules.test @@ -248,6 +248,10 @@ xyz() [out] main, line 1: No module named 'xyz' +[case testAccessingUnknownModuleIgnore] +import xyz # mypy: ignore +[out] + [case testAccessingUnknownModule2] import xyz, bar xyz.foo() From 7c3b5a3f4069dfa2a76dff33a9e71d50c8b8b499 Mon Sep 17 00:00:00 2001 From: Sander Kersten Date: Mon, 1 Dec 2014 21:17:29 +0100 Subject: [PATCH 2/4] Small fixes --- mypy/parse.py | 5 +++-- mypy/test/data/parse-annotation.test | 0 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 mypy/test/data/parse-annotation.test diff --git a/mypy/parse.py b/mypy/parse.py index 3cddda66a1e9..d1fbaa2c092c 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -34,7 +34,8 @@ parse_type, parse_types, parse_signature, TypeParseError ) from mypy.annotations import Annotation, IgnoreAnnotation -from mypy.parseannotation import parse_annotation +from mypy.parseannotation import parse_annotation, AnnotationParseError + precedence = { '**': 16, @@ -1731,7 +1732,7 @@ def parse_annotation_comment(self, token: Token) -> Annotation: return None try: annotation, index = parse_annotation(tokens, 0) - except TypeParseError as e: + except AnnotationParseError as e: self.parse_error_at(e.token, skip = False) return None if index < len(tokens) - 2: diff --git a/mypy/test/data/parse-annotation.test b/mypy/test/data/parse-annotation.test new file mode 100644 index 000000000000..e69de29bb2d1 From 428fff77ff84f668dda5d15934e54e6be68d513f Mon Sep 17 00:00:00 2001 From: Sander Kersten Date: Mon, 1 Dec 2014 21:18:20 +0100 Subject: [PATCH 3/4] Added tests for annotations --- mypy/test/data/parse-annotation.test | 17 +++++++++++++++++ mypy/test/testparse.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mypy/test/data/parse-annotation.test b/mypy/test/data/parse-annotation.test index e69de29bb2d1..bb579bfc7235 100644 --- a/mypy/test/data/parse-annotation.test +++ b/mypy/test/data/parse-annotation.test @@ -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] +, line 1: Empty annotation + +[case testInvalidAnnotation] +import xyz # mypy: xxx +[out] +, line 1: Parse error before "xxx" diff --git a/mypy/test/testparse.py b/mypy/test/testparse.py index 2a49de3c865c..8b9e11224fc5 100644 --- a/mypy/test/testparse.py +++ b/mypy/test/testparse.py @@ -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. From 53e6114452907ce2fd8273ce4b4f902d5284c3ca Mon Sep 17 00:00:00 2001 From: Sander Kersten Date: Mon, 1 Dec 2014 21:43:09 +0100 Subject: [PATCH 4/4] Added ignore annotation for `from x import y` statements --- mypy/parse.py | 4 +++- mypy/test/data/check-modules.test | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mypy/parse.py b/mypy/parse.py index d1fbaa2c092c..dc8d12b9b956 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -212,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)) diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test index 92c65023bbbc..69871d8e2626 100644 --- a/mypy/test/data/check-modules.test +++ b/mypy/test/data/check-modules.test @@ -250,6 +250,12 @@ 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]