Repository: PyCQA/pyflakes
Pull request #444 (2019-05-03 06:53:14)

namespace pyflakes's node monkeypatches
See https://gitlab.com/pycqa/flake8/issues/536 for full explanation

Basically bandit *also* monkeypatches `astnode.parent` which confuses `pyflakes`.  This manifests when running both of the tools as plugins inside of flake8.


Patch:
diff --git a/NEWS.rst b/NEWS.rst
index 8bf9023..993a545 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,7 @@
+2.1.1 (2019-02-28)
+- Fix reported line number for type comment errors
+- Fix typing.overload check to only check imported names
+
 2.1.0 (2019-01-23)
 
 - Allow intentional assignment to variables named ``_``
diff --git a/pyflakes/__init__.py b/pyflakes/__init__.py
index a33997d..55fa725 100644
--- a/pyflakes/__init__.py
+++ b/pyflakes/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '2.1.0'
+__version__ = '2.1.1'
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 0e636c1..acc2ae7 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -822,22 +822,31 @@ class Checker(object):
     def getParent(self, node):
         # Lookup the first parent which is not Tuple, List or Starred
         while True:
-            node = node.parent
+            node = node._pyflakes_parent
             if not hasattr(node, 'elts') and not hasattr(node, 'ctx'):
                 return node
 
     def getCommonAncestor(self, lnode, rnode, stop):
-        if stop in (lnode, rnode) or not (hasattr(lnode, 'parent') and
-                                          hasattr(rnode, 'parent')):
+        if (
+                stop in (lnode, rnode) or
+                not (
+                    hasattr(lnode, '_pyflakes_parent') and
+                    hasattr(rnode, '_pyflakes_parent')
+                )
+        ):
             return None
         if lnode is rnode:
             return lnode
 
-        if (lnode.depth > rnode.depth):
-            return self.getCommonAncestor(lnode.parent, rnode, stop)
-        if (lnode.depth < rnode.depth):
-            return self.getCommonAncestor(lnode, rnode.parent, stop)
-        return self.getCommonAncestor(lnode.parent, rnode.parent, stop)
+        if (lnode._pyflakes_depth > rnode._pyflakes_depth):
+            return self.getCommonAncestor(lnode._pyflakes_parent, rnode, stop)
+        if (lnode._pyflakes_depth < rnode._pyflakes_depth):
+            return self.getCommonAncestor(lnode, rnode._pyflakes_parent, stop)
+        return self.getCommonAncestor(
+            lnode._pyflakes_parent,
+            rnode._pyflakes_parent,
+            stop,
+        )
 
     def descendantOf(self, node, ancestors, stop):
         for a in ancestors:
@@ -875,7 +884,7 @@ class Checker(object):
         - `node` is the statement responsible for the change
         - `value` is the new value, a Binding instance
         """
-        # assert value.source in (node, node.parent):
+        # assert value.source in (node, node._pyflakes_parent):
         for scope in self.scopeStack[::-1]:
             if value.name in scope:
                 break
@@ -1014,11 +1023,11 @@ class Checker(object):
 
         parent_stmt = self.getParent(node)
         if isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or (
-                parent_stmt != node.parent and
+                parent_stmt != node._pyflakes_parent and
                 not self.isLiteralTupleUnpacking(parent_stmt)):
             binding = Binding(name, node)
         elif name == '__all__' and isinstance(self.scope, ModuleScope):
-            binding = ExportBinding(name, node.parent, self.scope)
+            binding = ExportBinding(name, node._pyflakes_parent, self.scope)
         elif isinstance(getattr(node, 'ctx', None), ast.Param):
             binding = Argument(name, self.getScopeNode(node))
         else:
@@ -1031,11 +1040,11 @@ class Checker(object):
             """
             Return `True` if node is part of a conditional body.
             """
-            current = getattr(node, 'parent', None)
+            current = getattr(node, '_pyflakes_parent', None)
             while current:
                 if isinstance(current, (ast.If, ast.While, ast.IfExp)):
                     return True
-                current = getattr(current, 'parent', None)
+                current = getattr(current, '_pyflakes_parent', None)
             return False
 
         name = getNodeName(node)
@@ -1122,8 +1131,8 @@ class Checker(object):
                                         self.isDocstring(node)):
             self.futuresAllowed = False
         self.nodeDepth += 1
-        node.depth = self.nodeDepth
-        node.parent = parent
+        node._pyflakes_depth = self.nodeDepth
+        node._pyflakes_parent = parent
         try:
             handler = self.getNodeHandler(node.__class__)
             handler(node)
@@ -1162,7 +1171,8 @@ class Checker(object):
         self.scopeStack = [self.scopeStack[0]]
         node_offset = self.offset or (0, 0)
         self.pushScope(DoctestScope)
-        self.addBinding(None, Builtin('_'))
+        if '_' not in self.scopeStack[0]:
+            self.addBinding(None, Builtin('_'))
         for example in examples:
             try:
                 tree = ast.parse(example.source, "<doctest>")
@@ -1355,7 +1365,7 @@ class Checker(object):
         if isinstance(node.ctx, (ast.Load, ast.AugLoad)):
             self.handleNodeLoad(node)
             if (node.id == 'locals' and isinstance(self.scope, FunctionScope) and
-                    isinstance(node.parent, ast.Call)):
+                    isinstance(node._pyflakes_parent, ast.Call)):
                 # we are doing locals() call in current scope
                 self.scope.usesLocals = True
         elif isinstance(node.ctx, (ast.Store, ast.AugStore, ast.Param)):
@@ -1371,8 +1381,8 @@ class Checker(object):
         # definition (not OK), for 'continue', a finally block (not OK), or
         # the top module scope (not OK)
         n = node
-        while hasattr(n, 'parent'):
-            n, n_child = n.parent, n
+        while hasattr(n, '_pyflakes_parent'):
+            n, n_child = n._pyflakes_parent, n
             if isinstance(n, LOOP_TYPES):
                 # Doesn't apply unless it's in the loop itself
                 if n_child not in n.orelse:
@@ -1480,7 +1490,7 @@ class Checker(object):
 
             self.pushScope()
 
-            self.handleChildren(node, omit='decorator_list')
+            self.handleChildren(node, omit=['decorator_list', 'returns'])
 
             def checkUnusedAssignments():
                 """


Test patch:
diff --git a/pyflakes/test/test_doctests.py b/pyflakes/test/test_doctests.py
index 0825ffe..5565226 100644
--- a/pyflakes/test/test_doctests.py
+++ b/pyflakes/test/test_doctests.py
@@ -433,6 +433,16 @@ class Test(TestCase):
             return 1
         ''')
 
+    def test_globalUnderscoreInDoctest(self):
+        self.flakes("""
+        from gettext import ugettext as _
+
+        def doctest_stuff():
+            '''
+                >>> pass
+            '''
+        """, m.UnusedImport)
+
 
 class TestOther(_DoctestMixin, TestOther):
     """Run TestOther with each test wrapped in a doctest."""
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index b8876cb..9c34dcf 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -77,24 +77,16 @@ class TestTypeAnnotations(TestCase):
     def test_not_a_typing_overload(self):
         """regression test for @typing.overload detection bug in 2.1.0"""
         self.flakes("""
-            x = lambda f: f
+            def foo(x):
+                return x
 
-            @x
-            def t():
+            @foo
+            def bar():
                 pass
 
-            y = lambda f: f
-
-            @x
-            @y
-            def t():
-                pass
-
-            @x
-            @y
-            def t():
+            def bar():
                 pass
-        """, m.RedefinedWhileUnused, m.RedefinedWhileUnused)
+        """, m.RedefinedWhileUnused)
 
     @skipIf(version_info < (3, 6), 'new in Python 3.6')
     def test_variable_annotations(self):
@@ -350,3 +342,23 @@ class TestTypeAnnotations(TestCase):
         x = 1
         # type: F
         """)
+
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_return_annotation_is_class_scope_variable(self):
+        self.flakes("""
+        from typing import TypeVar
+        class Test:
+            Y = TypeVar('Y')
+
+            def t(self, x: Y) -> Y:
+                return x
+        """)
+
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_return_annotation_is_function_body_variable(self):
+        self.flakes("""
+        class Test:
+            def t(self) -> Y:
+                Y = 2
+                return Y
+        """, m.UndefinedName)