Repository: PyCQA/pyflakes
Pull request #534 (2020-04-24 20:02:24)

Fix quoted type annotations in typing.TypeVar contexts
[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar) has such contexts in the positional arguments (for constraints) and in the `bound` keyword argument.


Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index 1738253..0000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# To activate, change the Appveyor settings to use `.appveyor.yml`.
-install:
-  - python -m pip install --upgrade tox virtualenv
-
-  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy2.7-v7.1.1-win32.zip")
-  - ps: 7z x pypy2.7-v7.1.1-win32.zip | Out-Null
-  - move pypy2.7-v7.1.1-win32 C:\
-  - 'SET PATH=C:\pypy2.7-v7.1.1-win32\;%PATH%'
-
-  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy3.6-v7.1.1-win32.zip")
-  - ps: 7z x pypy3.6-v7.1.1-win32.zip | Out-Null
-  - move pypy3.6-v7.1.1-win32 C:\
-  - 'SET PATH=C:\pypy3.6-v7.1.1-win32\;%PATH%'
-
-build: off
-
-test_script:
-  - python -m tox
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e51b2e0..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: python
-cache: pip
-matrix:
-  include:
-    - python: 2.7
-    - python: 3.4
-    - python: 3.5
-    - python: 3.6
-    - python: pypy
-    - python: pypy3
-    - python: 3.7
-      dist: xenial
-    - python: nightly
-      dist: xenial
-# TODO: https://bugs.python.org/issue40334
-# the PEG parser is currently broken in some cases
-  allow_failures:
-    - python: nightly
-      dist: xenial
-install: pip install tox
-script: tox -e py
diff --git a/README.rst b/README.rst
index ca802a8..c456d25 100644
--- a/README.rst
+++ b/README.rst
@@ -9,7 +9,7 @@ parsing the source file, not importing it, so it is safe to use on
 modules with side effects.  It's also much faster.
 
 It is `available on PyPI <https://pypi.org/project/pyflakes/>`_
-and it supports all active versions of Python: 2.7 and 3.4 to 3.7.
+and it supports all active versions of Python: 2.7 and 3.4 to 3.8.
 
 
 
@@ -72,13 +72,13 @@ rebase your commits for you.
 
 All changes should include tests and pass flake8_.
 
-.. image:: https://api.travis-ci.org/PyCQA/pyflakes.svg?branch=master
-   :target: https://travis-ci.org/PyCQA/pyflakes
-   :alt: Build status
+.. image:: https://github.com/PyCQA/pyflakes/workflows/Test/badge.svg
+   :target: https://github.com/PyCQA/pyflakes/actions
+   :alt: GitHub Actions build status
 
-.. _Pylint: http://www.pylint.org/
+.. _Pylint: https://www.pylint.org/
 .. _flake8: https://pypi.org/project/flake8/
-.. _`PEP 8`: http://legacy.python.org/dev/peps/pep-0008/
+.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
 .. _Pychecker: http://pychecker.sourceforge.net/
 .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 5af956c..095397b 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -79,6 +79,10 @@ else:
     LOOP_TYPES = (ast.While, ast.For)
     FUNCTION_TYPES = (ast.FunctionDef,)
 
+if PY36_PLUS:
+    ANNASSIGN_TYPES = (ast.AnnAssign,)
+else:
+    ANNASSIGN_TYPES = ()
 
 if PY38_PLUS:
     def _is_singleton(node):  # type: (ast.AST) -> bool
@@ -124,6 +128,13 @@ def _is_const_non_singleton(node):  # type: (ast.AST) -> bool
     return _is_constant(node) and not _is_singleton(node)
 
 
+def _is_name_or_attr(node, name):  # type: (ast.Ast, str) -> bool
+    return (
+        (isinstance(node, ast.Name) and node.id == name) or
+        (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
@@ -528,6 +539,16 @@ class Assignment(Binding):
     """
 
 
+class Annotation(Binding):
+    """
+    Represents binding a name to a type without an associated value.
+
+    As long as this name is not assigned a value in another binding, it is considered
+    undefined for most purposes. One notable exception is using the name as a type
+    annotation.
+    """
+
+
 class FunctionDefinition(Definition):
     pass
 
@@ -542,7 +563,7 @@ class ExportBinding(Binding):
     can be determined statically, they will be treated as names for export and
     additional checking applied to them.
 
-    The only recognized C{__all__} assignment via list concatenation is in the
+    The only recognized C{__all__} assignment via list/tuple concatenation is in the
     following format:
 
         __all__ = ['a'] + ['b'] + ['c']
@@ -564,10 +585,10 @@ class ExportBinding(Binding):
 
         if isinstance(source.value, (ast.List, ast.Tuple)):
             _add_to_names(source.value)
-        # If concatenating lists
+        # If concatenating lists or tuples
         elif isinstance(source.value, ast.BinOp):
             currentValue = source.value
-            while isinstance(currentValue.right, ast.List):
+            while isinstance(currentValue.right, (ast.List, ast.Tuple)):
                 left = currentValue.left
                 right = currentValue.right
                 _add_to_names(right)
@@ -575,7 +596,7 @@ class ExportBinding(Binding):
                 if isinstance(left, ast.BinOp):
                     currentValue = left
                 # If just two lists are being added
-                elif isinstance(left, ast.List):
+                elif isinstance(left, (ast.List, ast.Tuple)):
                     _add_to_names(left)
                     # All lists accounted for - done
                     break
@@ -648,6 +669,10 @@ class DummyNode(object):
         self.col_offset = col_offset
 
 
+class DetectClassScopedMagic:
+    names = dir()
+
+
 # Globally defined names which are not attributes of the builtins module, or
 # are only present on some platforms.
 _MAGIC_GLOBALS = ['__file__', '__builtins__', 'WindowsError']
@@ -732,6 +757,12 @@ def is_typing_overload(value, scope_stack):
     )
 
 
+class AnnotationState:
+    NONE = 0
+    STRING = 1
+    BARE = 2
+
+
 def in_annotation(func):
     @functools.wraps(func)
     def in_annotation_func(self, *args, **kwargs):
@@ -740,6 +771,14 @@ def in_annotation(func):
     return in_annotation_func
 
 
+def in_string_annotation(func):
+    @functools.wraps(func)
+    def in_annotation_func(self, *args, **kwargs):
+        with self._enter_annotation(AnnotationState.STRING):
+            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):
@@ -826,8 +865,7 @@ class Checker(object):
     nodeDepth = 0
     offset = None
     traceTree = False
-    _in_annotation = False
-    _in_typing_literal = False
+    _in_annotation = AnnotationState.NONE
     _in_deferred = False
 
     builtIns = set(builtin_vars).union(_MAGIC_GLOBALS)
@@ -1146,8 +1184,11 @@ class Checker(object):
                     # iteration
                     continue
 
-            if (name == 'print' and
-                    isinstance(scope.get(name, None), Builtin)):
+            binding = scope.get(name, None)
+            if isinstance(binding, Annotation) and not self._in_postponed_annotation:
+                continue
+
+            if name == 'print' and isinstance(binding, Builtin):
                 parent = self.getParent(node)
                 if (isinstance(parent, ast.BinOp) and
                         isinstance(parent.op, ast.RShift)):
@@ -1194,7 +1235,7 @@ class Checker(object):
             # the special name __path__ is valid only in packages
             return
 
-        if name == '__module__' and isinstance(self.scope, ClassScope):
+        if name in DetectClassScopedMagic.names and isinstance(self.scope, ClassScope):
             return
 
         # protected with a NameError handler?
@@ -1222,7 +1263,9 @@ class Checker(object):
                     break
 
         parent_stmt = self.getParent(node)
-        if isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or (
+        if isinstance(parent_stmt, ANNASSIGN_TYPES) and parent_stmt.value is None:
+            binding = Annotation(name, node)
+        elif isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or (
                 parent_stmt != node._pyflakes_parent and
                 not self.isLiteralTupleUnpacking(parent_stmt)):
             binding = Binding(name, node)
@@ -1265,13 +1308,20 @@ class Checker(object):
                 self.report(messages.UndefinedName, node, name)
 
     @contextlib.contextmanager
-    def _enter_annotation(self):
-        orig, self._in_annotation = self._in_annotation, True
+    def _enter_annotation(self, ann_type=AnnotationState.BARE):
+        orig, self._in_annotation = self._in_annotation, ann_type
         try:
             yield
         finally:
             self._in_annotation = orig
 
+    @property
+    def _in_postponed_annotation(self):
+        return (
+            self._in_annotation == AnnotationState.STRING or
+            self.annotationsFutureEnabled
+        )
+
     def _handle_type_comments(self, node):
         for (lineno, col_offset), comment in self._type_comments.get(node, ()):
             comment = comment.split(':', 1)[1].strip()
@@ -1399,7 +1449,7 @@ class Checker(object):
         self.popScope()
         self.scopeStack = saved_stack
 
-    @in_annotation
+    @in_string_annotation
     def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err):
         try:
             tree = ast.parse(s)
@@ -1457,20 +1507,36 @@ class Checker(object):
         STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren
 
     def SUBSCRIPT(self, node):
-        if (
-                (
-                    isinstance(node.value, ast.Name) and
-                    node.value.id == 'Literal'
-                ) or (
-                    isinstance(node.value, ast.Attribute) and
-                    node.value.attr == 'Literal'
-                )
-        ):
-            orig, self._in_typing_literal = self._in_typing_literal, True
-            try:
+        if _is_name_or_attr(node.value, 'Literal'):
+            with self._enter_annotation(AnnotationState.NONE):
                 self.handleChildren(node)
-            finally:
-                self._in_typing_literal = orig
+        elif _is_name_or_attr(node.value, 'Annotated'):
+            self.handleNode(node.value, node)
+
+            # py39+
+            if isinstance(node.slice, ast.Tuple):
+                slice_tuple = node.slice
+            # <py39
+            elif (
+                    isinstance(node.slice, ast.Index) and
+                    isinstance(node.slice.value, ast.Tuple)
+            ):
+                slice_tuple = node.slice.value
+            else:
+                slice_tuple = None
+
+            # not a multi-arg `Annotated`
+            if slice_tuple is None or len(slice_tuple.elts) < 2:
+                self.handleNode(node.slice, node)
+            else:
+                # the first argument is the type
+                self.handleNode(slice_tuple.elts[0], node)
+                # the rest of the arguments are not
+                with self._enter_annotation(AnnotationState.NONE):
+                    for arg in slice_tuple.elts[1:]:
+                        self.handleNode(arg, node)
+
+            self.handleNode(node.ctx, node)
         else:
             if _is_any_typing_member(node.value, self.scopeStack):
                 with self._enter_annotation():
@@ -1611,9 +1677,22 @@ class Checker(object):
             len(node.args) >= 1 and
             isinstance(node.args[0], ast.Str)
         ):
-            with self._enter_annotation():
+            with self._enter_annotation(AnnotationState.STRING):
                 self.handleNode(node.args[0], node)
 
+        elif _is_typing(node.func, 'TypeVar', self.scopeStack):
+            # TypeVar("T", "int", "str")
+            for arg in node.args[1:]:
+                if isinstance(arg, ast.Str):
+                    with self._enter_annotation():
+                        self.handleNode(arg, node)
+
+            # TypeVar("T", bound="str")
+            for keyword in node.keywords:
+                if keyword.arg == 'bound' and isinstance(keyword.value, ast.Str):
+                    with self._enter_annotation():
+                        self.handleNode(keyword.value, node)
+
         self.handleChildren(node)
 
     def _handle_percent_format(self, node):
@@ -1728,7 +1807,7 @@ class Checker(object):
         self.handleChildren(node)
 
     def STR(self, node):
-        if self._in_annotation and not self._in_typing_literal:
+        if self._in_annotation:
             fn = functools.partial(
                 self.handleStringAnnotation,
                 node.s,
@@ -2224,11 +2303,7 @@ class Checker(object):
             self.scope[node.name] = prev_definition
 
     def ANNASSIGN(self, node):
-        if node.value:
-            # Only bind the *targets* if the assignment has a value.
-            # Otherwise it's not really ast.Store and shouldn't silence
-            # UndefinedLocal warnings.
-            self.handleNode(node.target, node)
+        self.handleNode(node.target, node)
         self.handleAnnotation(node.annotation, node)
         if node.value:
             # If the assignment has value, handle the *value* now.
diff --git a/setup.py b/setup.py
index 5e2088e..e4d709d 100755
--- a/setup.py
+++ b/setup.py
@@ -58,6 +58,7 @@ setup(
         "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: Software Development",
diff --git a/tox.ini b/tox.ini
index f2256c8..37f2b86 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,13 @@
 [tox]
 skip_missing_interpreters = True
 envlist =
-    py27,py34,py35,py36,py37,pypy,pypy3
+    py27,py34,py35,py36,py37,py38,pypy,pypy3
 
 [testenv]
 deps = flake8==3.6.0
 setenv = PYFLAKES_ERROR_UNKNOWN=1
 commands =
+    python --version --version
     python -m unittest discover pyflakes
     flake8 pyflakes setup.py
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..39ba139
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,33 @@
+name: Test
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy2", "pypy3"]
+        os: [ubuntu-latest]
+        # Include py2 + minimum py3 + maximum py3 + pypy + pypy3 on Windows
+        include:
+          - { os: "windows-latest" , python-version: "2.7" }
+          - { os: "windows-latest" , python-version: "3.5" }
+          - { os: "windows-latest" , python-version: "3.9" }
+          - { os: "windows-latest" , python-version: "pypy2" }
+          - { os: "windows-latest" , python-version: "pypy3" }
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: python -m pip install -U tox
+
+      - name: Tox tests
+        run: tox -e py
diff --git a/pyflakes/test/test_api.py b/pyflakes/test/test_api.py
index b579ac8..d379b3b 100644
--- a/pyflakes/test/test_api.py
+++ b/pyflakes/test/test_api.py
@@ -449,8 +449,6 @@ def baz():
         with self.makeTempFile(source) as sourcePath:
             if PYPY:
                 message = 'end of file (EOF) while scanning triple-quoted string literal'
-            elif sys.version_info >= (3, 9):
-                message = 'invalid string prefix'
             else:
                 message = 'invalid syntax'
 
@@ -515,8 +513,10 @@ def foo(bar=baz, bax):
 """
         with self.makeTempFile(source) as sourcePath:
             if ERROR_HAS_LAST_LINE:
-                if PYPY and sys.version_info >= (3,):
+                if PYPY:
                     column = 7
+                elif sys.version_info >= (3, 9):
+                    column = 21
                 elif sys.version_info >= (3, 8):
                     column = 9
                 else:
@@ -543,8 +543,10 @@ foo(bar=baz, bax)
 """
         with self.makeTempFile(source) as sourcePath:
             if ERROR_HAS_LAST_LINE:
-                if PYPY and sys.version_info >= (3,):
+                if PYPY:
                     column = 12
+                elif sys.version_info >= (3, 9):
+                    column = 17
                 elif sys.version_info >= (3, 8):
                     column = 14
                 else:
@@ -578,7 +580,9 @@ foo(bar=baz, bax)
             else:
                 position_end = 1
                 if PYPY:
-                    column = 6
+                    column = 5
+                elif ver >= (3, 9):
+                    column = 13
                 else:
                     column = 7
                     # Column has been "fixed" since 3.2.4 and 3.3.1
@@ -717,13 +721,6 @@ class IntegrationTests(TestCase):
     """
     Tests of the pyflakes script that actually spawn the script.
     """
-
-    # https://bitbucket.org/pypy/pypy/issues/3069/pypy36-on-windows-incorrect-line-separator
-    if PYPY and sys.version_info >= (3,) and WIN:
-        LINESEP = '\n'
-    else:
-        LINESEP = os.linesep
-
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
         self.tempfilepath = os.path.join(self.tempdir, 'temp')
@@ -784,7 +781,7 @@ class IntegrationTests(TestCase):
             fd.write("import contraband\n".encode('ascii'))
         d = self.runPyflakes([self.tempfilepath])
         expected = UnusedImport(self.tempfilepath, Node(1), 'contraband')
-        self.assertEqual(d, ("%s%s" % (expected, self.LINESEP), '', 1))
+        self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
 
     def test_errors_io(self):
         """
@@ -794,7 +791,7 @@ class IntegrationTests(TestCase):
         """
         d = self.runPyflakes([self.tempfilepath])
         error_msg = '%s: No such file or directory%s' % (self.tempfilepath,
-                                                         self.LINESEP)
+                                                         os.linesep)
         self.assertEqual(d, ('', error_msg, 1))
 
     def test_errors_syntax(self):
@@ -807,7 +804,7 @@ class IntegrationTests(TestCase):
             fd.write("import".encode('ascii'))
         d = self.runPyflakes([self.tempfilepath])
         error_msg = '{0}:1:{2}: invalid syntax{1}import{1}     {3}^{1}'.format(
-            self.tempfilepath, self.LINESEP, 6 if PYPY else 7, '' if PYPY else ' ')
+            self.tempfilepath, os.linesep, 6 if PYPY else 7, '' if PYPY else ' ')
         self.assertEqual(d, ('', error_msg, 1))
 
     def test_readFromStdin(self):
@@ -816,15 +813,13 @@ class IntegrationTests(TestCase):
         """
         d = self.runPyflakes([], stdin='import contraband')
         expected = UnusedImport('<stdin>', Node(1), 'contraband')
-        self.assertEqual(d, ("%s%s" % (expected, self.LINESEP), '', 1))
+        self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
 
 
 class TestMain(IntegrationTests):
     """
     Tests of the pyflakes main function.
     """
-    LINESEP = os.linesep
-
     def runPyflakes(self, paths, stdin=None):
         try:
             with SysStreamCapturing(stdin) as capture:
diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py
index 13e7bef..d5be269 100644
--- a/pyflakes/test/test_imports.py
+++ b/pyflakes/test/test_imports.py
@@ -1084,7 +1084,7 @@ class TestSpecialAll(TestCase):
             __all__ += ['c', 'd']
         ''', m.UndefinedExport, m.UndefinedExport)
 
-    def test_concatenationAssignment(self):
+    def test_list_concatenation_assignment(self):
         """
         The C{__all__} variable is defined through list concatenation.
         """
@@ -1093,6 +1093,15 @@ class TestSpecialAll(TestCase):
         __all__ = ['a'] + ['b'] + ['c']
         ''', m.UndefinedExport, m.UndefinedExport, m.UndefinedExport, m.UnusedImport)
 
+    def test_tuple_concatenation_assignment(self):
+        """
+        The C{__all__} variable is defined through tuple concatenation.
+        """
+        self.flakes('''
+        import sys
+        __all__ = ('a',) + ('b',) + ('c',)
+        ''', m.UndefinedExport, m.UndefinedExport, m.UndefinedExport, m.UnusedImport)
+
     def test_all_with_attributes(self):
         self.flakes('''
         from foo import bar
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index ed28127..409238b 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -263,6 +263,14 @@ class TestTypeAnnotations(TestCase):
         class A: pass
         ''')
         self.flakes('''
+        T: object
+        def f(t: T): pass
+        ''', m.UndefinedName)
+        self.flakes('''
+        T: object
+        def g(t: 'T'): pass
+        ''')
+        self.flakes('''
         a: 'A B'
         ''', m.ForwardAnnotationSyntaxError)
         self.flakes('''
@@ -275,6 +283,26 @@ class TestTypeAnnotations(TestCase):
         a: 'a: "A"'
         ''', m.ForwardAnnotationSyntaxError)
 
+    @skipIf(version_info < (3, 6), 'new in Python 3.6')
+    def test_unused_annotation(self):
+        # Unused annotations are fine in module and class scope
+        self.flakes('''
+        x: int
+        class Cls:
+            y: int
+        ''')
+        # TODO: this should print a UnusedVariable message
+        self.flakes('''
+        def f():
+            x: int
+        ''')
+        # This should only print one UnusedVariable message
+        self.flakes('''
+        def f():
+            x: int
+            x = 3
+        ''', m.UnusedVariable)
+
     @skipIf(version_info < (3, 5), 'new in Python 3.5')
     def test_annotated_async_def(self):
         self.flakes('''
@@ -300,6 +328,13 @@ class TestTypeAnnotations(TestCase):
         class B: pass
         ''', m.UndefinedName)
 
+        self.flakes('''
+        from __future__ import annotations
+        T: object
+        def f(t: T): pass
+        def g(t: 'T'): pass
+        ''')
+
     def test_typeCommentsMarkImportsAsUsed(self):
         self.flakes("""
         from mod import A, B, C, D, E, F, G
@@ -489,6 +524,21 @@ class TestTypeAnnotations(TestCase):
         maybe_int = tsac('Maybe[int]', 42)
         """)
 
+    def test_quoted_TypeVar_constraints(self):
+        self.flakes("""
+        from typing import TypeVar, Optional
+
+        T = TypeVar('T', 'str', 'Optional[int]', bytes)
+        """)
+
+    def test_quoted_TypeVar_bound(self):
+        self.flakes("""
+        from typing import TypeVar, Optional, List
+
+        T = TypeVar('T', bound='Optional[int]')
+        S = TypeVar('S', int, bound='List[int]')
+        """)
+
     @skipIf(version_info < (3,), 'new in Python 3')
     def test_literal_type_typing(self):
         self.flakes("""
@@ -507,6 +557,42 @@ class TestTypeAnnotations(TestCase):
             return None
         """)
 
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_annotated_type_typing_missing_forward_type(self):
+        self.flakes("""
+        from typing import Annotated
+
+        def f(x: Annotated['integer']) -> None:
+            return None
+        """, m.UndefinedName)
+
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_annotated_type_typing_missing_forward_type_multiple_args(self):
+        self.flakes("""
+        from typing import Annotated
+
+        def f(x: Annotated['integer', 1]) -> None:
+            return None
+        """, m.UndefinedName)
+
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_annotated_type_typing_with_string_args(self):
+        self.flakes("""
+        from typing import Annotated
+
+        def f(x: Annotated[int, '> 0']) -> None:
+            return None
+        """)
+
+    @skipIf(version_info < (3,), 'new in Python 3')
+    def test_annotated_type_typing_with_string_args_in_union(self):
+        self.flakes("""
+        from typing import Annotated, Union
+
+        def f(x: Union[Annotated['int', '>0'], 'integer']) -> None:
+            return None
+        """, m.UndefinedName)
+
     @skipIf(version_info < (3,), 'new in Python 3')
     def test_literal_type_some_other_module(self):
         """err on the side of false-negatives for types named Literal"""
@@ -552,3 +638,47 @@ class TestTypeAnnotations(TestCase):
             def f() -> Optional['Queue[str]']:
                 return None
         """)
+
+    def test_idomiatic_typing_guards(self):
+        # typing.TYPE_CHECKING: python3.5.3+
+        self.flakes("""
+            from typing import TYPE_CHECKING
+
+            if TYPE_CHECKING:
+                from t import T
+
+            def f():  # type: () -> T
+                pass
+        """)
+        # False: the old, more-compatible approach
+        self.flakes("""
+            if False:
+                from t import T
+
+            def f():  # type: () -> T
+                pass
+        """)
+        # some choose to assign a constant and do it that way
+        self.flakes("""
+            MYPY = False
+
+            if MYPY:
+                from t import T
+
+            def f():  # type: () -> T
+                pass
+        """)
+
+    def test_typing_guard_for_protocol(self):
+        self.flakes("""
+            from typing import TYPE_CHECKING
+
+            if TYPE_CHECKING:
+                from typing import Protocol
+            else:
+                Protocol = object
+
+            class C(Protocol):
+                def f():  # type: () -> int
+                    pass
+        """)
diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py
index c952cbb..e0e628d 100644
--- a/pyflakes/test/test_undefined_names.py
+++ b/pyflakes/test/test_undefined_names.py
@@ -279,6 +279,23 @@ class Test(TestCase):
                 __module__
         ''', m.UndefinedName)
 
+    @skipIf(version_info < (3, 3), "Python >= 3.3 only")
+    def test_magicQualnameInClassScope(self):
+        """
+        Use of the C{__qualname__} magic builtin should not emit an undefined
+        name warning if used in class scope.
+        """
+        self.flakes('__qualname__', m.UndefinedName)
+        self.flakes('''
+        class Foo:
+            __qualname__
+        ''')
+        self.flakes('''
+        class Foo:
+            def bar(self):
+                __qualname__
+        ''', m.UndefinedName)
+
     def test_globalImportStar(self):
         """Can't find undefined names with import *."""
         self.flakes('from fu import *; bar',