Repository: PyCQA/pyflakes (python)

Issue:
Error when referencing a class yet not declared inside function
I am facing a bug.

I created this example to test all behaviours I can foresee and spotted a bug:

```python
from typing import Optional

class InnerModel1:
        model2: "Model2"
        mode2_no_err1: Optional["Model2"] # NO ERROR
        mode2_no_err2: "Optional[Model2]" # NO ERROR

class Model2:
    pass

def f():
    class InnerModel1:
        model2: "InnerModel2"
        mode2_err: Optional["InnerModel2"] # ERROR
        mode2_no_err: "Optional[InnerModel2]"

    class InnerModel2:
        pass
```
if I run `pyflakes` against this code I get:

`pyflakes_bug.py:14:29: undefined name 'InnerModel2'`

I would expect that annotating a with `Optional["InnerModel2"]` or with `"Optional[InnerModel2]"` should have the same behaviour as demonstrated with `Optional["Model2"]` and `"Optional[Model2]"`.

Why am I defining a class inside a method?  For now this is supporting lots of unit testing on my side.




Pull request:
handle deferred checking as a queue
resolves #749

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 919e1bb..7ee6a1d 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -7,6 +7,7 @@ Also, it models the Bindings and Scopes.
 import __future__
 import builtins
 import ast
+import collections
 import contextlib
 import doctest
 import functools
@@ -735,7 +736,6 @@ class Checker:
     nodeDepth = 0
     offset = None
     _in_annotation = AnnotationState.NONE
-    _in_deferred = False
 
     builtIns = set(builtin_vars).union(_MAGIC_GLOBALS)
     _customBuiltIns = os.environ.get('PYFLAKES_BUILTINS')
@@ -746,7 +746,7 @@ class Checker:
     def __init__(self, tree, filename='(none)', builtins=None,
                  withDoctest='PYFLAKES_DOCTEST' in os.environ, file_tokens=()):
         self._nodeHandlers = {}
-        self._deferredFunctions = []
+        self._deferred = collections.deque()
         self.deadScopes = []
         self.messages = []
         self.filename = filename
@@ -762,12 +762,8 @@ class Checker:
         for builtin in self.builtIns:
             self.addBinding(None, Builtin(builtin))
         self.handleChildren(tree)
-        self._in_deferred = True
-        self.runDeferred(self._deferredFunctions)
-        # Set _deferredFunctions to None so that deferFunction will fail
-        # noisily if called after we've run through the deferred functions.
-        self._deferredFunctions = None
-        del self.scopeStack[1:]
+
+        self._run_deferred()
 
         self.popScope()
         self.checkDeadScopes()
@@ -787,17 +783,18 @@ class Checker:
         `callable` is called, the scope at the time this is called will be
         restored, however it will contain any new bindings added to it.
         """
-        self._deferredFunctions.append((callable, self.scopeStack[:], self.offset))
+        self._deferred.append((callable, self.scopeStack[:], self.offset))
 
-    def runDeferred(self, deferred):
-        """
-        Run the callables in C{deferred} using their associated scope stack.
-        """
-        for handler, scope, offset in deferred:
-            self.scopeStack = scope
-            self.offset = offset
+    def _run_deferred(self):
+        orig = (self.scopeStack, self.offset)
+
+        while self._deferred:
+            handler, scope, offset = self._deferred.popleft()
+            self.scopeStack, self.offset = scope, offset
             handler()
 
+        self.scopeStack, self.offset = orig
+
     def _in_doctest(self):
         return (len(self.scopeStack) >= 2 and
                 isinstance(self.scopeStack[1], DoctestScope))
@@ -1696,10 +1693,7 @@ class Checker:
                 node.col_offset,
                 messages.ForwardAnnotationSyntaxError,
             )
-            if self._in_deferred:
-                fn()
-            else:
-                self.deferFunction(fn)
+            self.deferFunction(fn)
 
     def CONSTANT(self, node):
         if isinstance(node.value, str):


Test patch:
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index 2f27b06..f0fd3b9 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -594,6 +594,20 @@ class TestTypeAnnotations(TestCase):
                 return None
         """)
 
+    def test_forward_annotations_for_classes_in_scope(self):
+        # see #749
+        self.flakes("""
+        from typing import Optional
+
+        def f():
+            class C:
+                a: "D"
+                b: Optional["D"]
+                c: "Optional[D]"
+
+            class D: pass
+        """)
+
     def test_idomiatic_typing_guards(self):
         # typing.TYPE_CHECKING: python3.5.3+
         self.flakes("""