Hi,
I guess the change in pyflakes is intended, please see [1][2].
The FTBFS for src:python-apt 1.7.0 downloaded from shanpshot.d.o [3]
vanishes after rebuilding with this commit reverted [4] (attached
for saving your time and for double-checking purpose).
Kind Regards
[1] https://github.com/PyCQA/pyflakes/issues/747
[2] https://data.safetycli.com/packages/pypi/pyflakes/changelog
[3] https://snapshot.debian.org/package/python-apt/2.7.0/
[4] https://github.com/PyCQA/pyflakes/pull/684
--- a/pyflakes/api.py
+++ b/pyflakes/api.py
@@ -44,7 +44,8 @@
reporter.unexpectedError(filename, 'problem decoding source')
return 1
# Okay, it's syntactically valid. Now check it.
- w = checker.Checker(tree, filename=filename)
+ file_tokens = checker.make_tokens(codeString)
+ w = checker.Checker(tree, file_tokens=file_tokens, filename=filename)
w.messages.sort(key=lambda m: m.lineno)
for warning in w.messages:
reporter.flake(warning)
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -15,6 +15,9 @@
import re
import string
import sys
+import bisect
+import collections
+import tokenize
import warnings
from pyflakes import messages
@@ -66,6 +69,14 @@
(isinstance(node, ast.Attribute) and node.attr == name)
)
+# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L102-L104
+TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*')
+# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413
+ASCII_NON_ALNUM = ''.join([chr(i) for i in range(128) if not chr(i).isalnum()])
+TYPE_IGNORE_RE = re.compile(
+ TYPE_COMMENT_RE.pattern + fr'ignore([{ASCII_NON_ALNUM}]|$)')
+# https://github.com/python/typed_ast/blob/1.4.0/ast27/Grammar/Grammar#L147
+TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$')
MAPPING_KEY_RE = re.compile(r'\(([^()]*)\)')
CONVERSION_FLAG_RE = re.compile('[#0+ -]*')
@@ -594,6 +605,11 @@
class DoctestScope(ModuleScope):
"""Scope for a doctest."""
+class DummyNode:
+ """Used in place of an `ast.AST` to set error message positions"""
+ def __init__(self, lineno, col_offset):
+ self.lineno = lineno
+ self.col_offset = col_offset
class DetectClassScopedMagic:
names = dir()
@@ -713,6 +729,60 @@
return func(self, *args, **kwargs)
return in_annotation_func
+def make_tokens(code):
+ # PY3: tokenize.tokenize requires readline of bytes
+ if not isinstance(code, bytes):
+ code = code.encode('UTF-8')
+ lines = iter(code.splitlines(True))
+ # next(lines, b'') is to prevent an error in pypy3
+ return tuple(tokenize.tokenize(lambda: next(lines, b'')))
+
+
+class _TypeableVisitor(ast.NodeVisitor):
+ """Collect the line number and nodes which are deemed typeable by
+ PEP 484
+ https://www.python.org/dev/peps/pep-0484/#type-comments
+ """
+ def __init__(self):
+ self.typeable_lines = []
+ self.typeable_nodes = {}
+
+ def _typeable(self, node):
+ # if there is more than one typeable thing on a line last one wins
+ self.typeable_lines.append(node.lineno)
+ self.typeable_nodes[node.lineno] = node
+
+ self.generic_visit(node)
+
+ visit_Assign = visit_For = visit_FunctionDef = visit_With = _typeable
+ visit_AsyncFor = visit_AsyncFunctionDef = visit_AsyncWith = _typeable
+
+
+def _collect_type_comments(tree, tokens):
+ visitor = _TypeableVisitor()
+ visitor.visit(tree)
+
+ type_comments = collections.defaultdict(list)
+ for tp, text, start, _, _ in tokens:
+ if (
+ tp != tokenize.COMMENT or # skip non comments
+ not TYPE_COMMENT_RE.match(text) or # skip non-type comments
+ TYPE_IGNORE_RE.match(text) # skip ignores
+ ):
+ continue
+
+ # search for the typeable node at or before the line number of the
+ # type comment.
+ # if the bisection insertion point is before any nodes this is an
+ # invalid type comment which is ignored.
+ lineno, _ = start
+ idx = bisect.bisect_right(visitor.typeable_lines, lineno)
+ if idx == 0:
+ continue
+ node = visitor.typeable_nodes[visitor.typeable_lines[idx - 1]]
+ type_comments[node].append((start, text))
+
+ return type_comments
class Checker:
"""I check the cleanliness and sanity of Python code."""
@@ -751,6 +821,7 @@
self.withDoctest = withDoctest
self.exceptHandlers = [()]
self.root = tree
+ self._type_comments = _collect_type_comments(tree, file_tokens)
self.scopeStack = []
try:
@@ -783,6 +854,25 @@
"""
self._deferred.append((callable, self.scopeStack[:], self.offset))
+ def _handle_type_comments(self, node):
+ for (lineno, col_offset), comment in self._type_comments.get(node, ()):
+ comment = comment.split(':', 1)[1].strip()
+ func_match = TYPE_FUNC_RE.match(comment)
+ if func_match:
+ parts = (
+ func_match.group(1).replace('*', ''),
+ func_match.group(2).strip(),
+ )
+ else:
+ parts = (comment,)
+
+ for part in parts:
+ self.deferFunction(functools.partial(
+ self.handleStringAnnotation,
+ part, DummyNode(lineno, col_offset), lineno, col_offset,
+ messages.CommentAnnotationSyntaxError,
+ ))
+
def _run_deferred(self):
orig = (self.scopeStack, self.offset)
@@ -1210,6 +1300,7 @@
)
def handleChildren(self, tree, omit=None):
+ self._handle_type_comments(tree)
for node in iter_child_nodes(tree, omit=omit):
self.handleNode(node, tree)
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -240,6 +240,12 @@
Message.__init__(self, filename, loc)
self.message_args = (annotation,)
+class CommentAnnotationSyntaxError(Message):
+ message = 'syntax error in type comment %r'
+
+ def __init__(self, filename, loc, annotation):
+ Message.__init__(self, filename, loc)
+ self.message_args = (annotation,)
class RaiseNotImplemented(Message):
message = "'raise NotImplemented' should be 'raise NotImplementedError'"