Repository: PyCQA/pyflakes

Issue:
Drop support for EOL Python versions?
I'd like to suggest pyflakes only supports non-EOL Python versions.

Python 2.5, 2.6, 3.2 and 3.3 are EOL and no longer receive security updates.

![image](https://user-images.githubusercontent.com/1324225/33826009-275286e4-de6c-11e7-8f70-781433006423.png)

https://github.com/PyCQA/pyflakes#pyflakes says:

> It is available on PyPI and it supports all active versions of Python from 2.5 to 3.6.

It can be argued that 2.5-2.6 and 3.0-3.3 are no longer active and no longer need support.

Here's the pip installs for pyflakes from PyPI for the last month (via `pypinfo --percent --pip --markdown pyflakes pyversion`) showing low or non-existent numbers for the EOL versions.

| python_version | percent | download_count |
| -------------- | ------: | -------------: |
| 2.7            |   54.1% |        582,452 |
| 3.6            |   20.5% |        221,023 |
| 3.5            |   17.2% |        185,122 |
| 3.4            |    6.8% |         72,930 |
| 3.3            |    0.6% |          6,432 |
| 2.6            |    0.4% |          4,789 |
| 3.7            |    0.4% |          4,226 |
| 3.2            |    0.0% |            414 |
| None           |    0.0% |              4 |

Some other PyCQA projects are planning on ending all Python 2 support around mid-2018 when Python 3.7 is due out (https://github.com/PyCQA/meta/issues/19).

Discussion:
bitglue: Also, we should probably merge this and #303 together and release a new major version since it breaks backwards compatibility.
hugovk: Appveyor looks like it needs an updated pip to recognise the new python_requires. I had a go updating it in another branch, but there's a pypy bug preventing that. They've just fixed it, and it'll be in the next pypy release, 5.10.1. 


See https://bitbucket.org/pypy/pypy/issues/2720/ensurepip-on-pypy-c-jit-93579-a4194a67868f and https://stackoverflow.com/q/47999518/724176.

I do not know when that will be out. Options are to wait until it's sorted until merge, or comment out that pypy build until then. (Merge with the failure doesn't really help.) 
bitglue: Let's just comment that build with an explanation for now.
hugovk: Commented out!

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 41b1dbb..8a62cf8 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -13,18 +13,21 @@ install:
   - ps: 7z x pypy2-v5.3.1-win32.zip | Out-Null
   - move pypy2-v5.3.1-win32 C:\
 
-  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy3-2.4.0-win32.zip', "$env:appveyor_build_folder\pypy3-2.4.0-win32.zip")
-  - ps: 7z x pypy3-2.4.0-win32.zip | Out-Null
-  - move pypy3-2.4.0-win32 C:\
+  # Note: pypy3-v5.10.0 is commented out as it needs an updated pip to
+  # recognise the new python_requires, but there's a pypy bug preventing that.
+  # It's been fixed and will be in the next 5.10.1 release.
+  # See https://bitbucket.org/pypy/pypy/issues/2720/ensurepip-on-pypy-c-jit-93579-a4194a67868f
+  # and https://stackoverflow.com/q/47999518/724176.
 
-  # pypy3 installer provides 'pypy.exe', not pypy3.exe.
-  - copy C:\pypy3-2.4.0-win32\pypy.exe C:\pypy3-2.4.0-win32\pypy3.exe
+#  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy3-v5.10.0-win32.zip', "$env:appveyor_build_folder\pypy3-v5.10.0-win32.zip")
+#  - ps: 7z x pypy3-v5.10.0-win32.zip | Out-Null
+#  - move pypy3-v5.10.0-win32 C:\
 
   # workaround https://github.com/pypa/virtualenv/issues/93
   - mkdir C:\python33\tcl\tcl8.6
   - mkdir C:\python33\tcl\tk8.6
-  - mkdir C:\pypy3-2.4.0-win32\tcl\tcl8.6
-  - mkdir C:\pypy3-2.4.0-win32\tcl\tk8.6
+#  - mkdir C:\pypy3-v5.10.0-win32\tcl\tcl8.6
+#  - mkdir C:\pypy3-v5.10.0-win32\tcl\tk8.6
 
   # Only pypy2-5.3.1 is integrated into tox, as pypy3-2.4.0 fails and
   # a Windows distribution of pypy3-5.2 isnt available yet.
@@ -33,14 +36,13 @@ install:
   # pypy3-2.4.0 and pypy-2.6.1 are manually bootstrapped and tested
   - ps: (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "$env:appveyor_build_folder\get-pip.py")
   # errors are ignored due to https://github.com/pypa/pip/issues/2669#issuecomment-136405390
-  # NOTE: If and when a new version of PyPy3 is released for Windows that
-  #       supports anything newer than Python 3.2, remove the setuptools pin.
-  - ps: C:\pypy3-2.4.0-win32\pypy3 "$env:appveyor_build_folder\get-pip.py"; C:\pypy3-2.4.0-win32\pypy3 -m pip install -U --force-reinstall pip==8.1.2 "setuptools<30"; echo "ignore error"
+#  - ps: C:\pypy3-v5.10.0-win32\pypy3 "$env:appveyor_build_folder\get-pip.py"; echo "ignore error"
+#  - ps: C:\pypy3-v5.10.0-win32\pypy3 -m pip install -U --force-reinstall pip setuptools; echo "ignore error"
   - ps: C:\pypy-2.6.1-win32\pypy "$env:appveyor_build_folder\get-pip.py"
 
 build: off
 
 test_script:
   - python -m tox
-  - C:\pypy3-2.4.0-win32\pypy3 setup.py test -q
+#  - C:\pypy3-v5.10.0-win32\pypy3 setup.py test -q
   - C:\pypy-2.6.1-win32\pypy setup.py test -q
diff --git a/.travis.yml b/.travis.yml
index 780b117..29f1d44 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,7 @@
 language: python
 cache: pip
 python:
-  - 2.6
   - 2.7
-  - 3.3
   - 3.4
   - 3.5
   - 3.6
@@ -11,13 +9,10 @@ python:
   - pypy
   - pypy-5.3
   - pypy3
-  - pypy3.3-5.2-alpha1
 install:
-  - pip install flake8==2.1.0 pep8==1.5.6
-  - python setup.py install
+  - pip install --upgrade .
   - pip list
-  - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then flake8 --version; fi
 script:
   - python setup.py test -q
-  - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then flake8 pyflakes setup.py; fi
+  - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then pip install flake8==2.1.0 pep8==1.5.6 && flake8 --version && flake8 pyflakes setup.py; fi
 sudo: false
diff --git a/README.rst b/README.rst
index 45f0ab8..6699b4c 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.python.org/pypi/pyflakes>`_
-and it supports all active versions of Python from 2.5 to 3.6.
+and it supports all active versions of Python: 2.7 and 3.4 to 3.6.
 
 
 
diff --git a/pyflakes/__main__.py b/pyflakes/__main__.py
index a69e689..68cd9ef 100644
--- a/pyflakes/__main__.py
+++ b/pyflakes/__main__.py
@@ -1,5 +1,5 @@
 from pyflakes.api import main
 
-# python -m pyflakes (with Python >= 2.7)
+# python -m pyflakes
 if __name__ == '__main__':
     main(prog='pyflakes')
diff --git a/pyflakes/api.py b/pyflakes/api.py
index 49ee38d..805a886 100644
--- a/pyflakes/api.py
+++ b/pyflakes/api.py
@@ -89,19 +89,8 @@ def checkPath(filename, reporter=None):
     if reporter is None:
         reporter = modReporter._makeDefaultReporter()
     try:
-        # in Python 2.6, compile() will choke on \r\n line endings. In later
-        # versions of python it's smarter, and we want binary mode to give
-        # compile() the best opportunity to do the right thing WRT text
-        # encodings.
-        if sys.version_info < (2, 7):
-            mode = 'rU'
-        else:
-            mode = 'rb'
-
-        with open(filename, mode) as f:
+        with open(filename, 'rb') as f:
             codestr = f.read()
-        if sys.version_info < (2, 7):
-            codestr += '\n'     # Work around for Python <= 2.6
     except UnicodeError:
         reporter.unexpectedError(filename, 'problem decoding source')
         return 1
@@ -117,6 +106,10 @@ def isPythonFile(filename):
     if filename.endswith('.py'):
         return True
 
+    # Avoid obvious Emacs backup files
+    if filename.endswith("~"):
+        return False
+
     max_bytes = 128
 
     try:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index baef833..bd5eba5 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -5,14 +5,13 @@ Implement the central Checker class.
 Also, it models the Bindings and Scopes.
 """
 import __future__
+import ast
 import doctest
 import os
 import sys
 
 PY2 = sys.version_info < (3, 0)
-PY32 = sys.version_info < (3, 3)    # Python 2.5 to 3.2
-PY33 = sys.version_info < (3, 4)    # Python 2.5 to 3.3
-PY34 = sys.version_info < (3, 5)    # Python 2.5 to 3.4
+PY34 = sys.version_info < (3, 5)    # Python 2.7 to 3.4
 try:
     sys.pypy_version_info
     PYPY = True
@@ -21,16 +20,6 @@ except AttributeError:
 
 builtin_vars = dir(__import__('__builtin__' if PY2 else 'builtins'))
 
-try:
-    import ast
-except ImportError:     # Python 2.5
-    import _ast as ast
-
-    if 'decorator_list' not in ast.ClassDef._fields:
-        # Patch the missing attribute 'decorator_list'
-        ast.ClassDef.decorator_list = ()
-        ast.FunctionDef.decorator_list = property(lambda s: s.decorators)
-
 from pyflakes import messages
 
 
@@ -38,15 +27,22 @@ if PY2:
     def getNodeType(node_class):
         # workaround str.upper() which is locale-dependent
         return str(unicode(node_class.__name__).upper())
+
+    def get_raise_argument(node):
+        return node.type
+
 else:
     def getNodeType(node_class):
         return node_class.__name__.upper()
 
+    def get_raise_argument(node):
+        return node.exc
+
     # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3.
     unicode = str
 
 # Python >= 3.3 uses ast.Try instead of (ast.TryExcept + ast.TryFinally)
-if PY32:
+if PY2:
     def getAlternatives(n):
         if isinstance(n, (ast.If, ast.TryFinally)):
             return [n.body]
@@ -131,13 +127,17 @@ def convert_to_value(item):
             result.name,
             result,
         )
-    elif (not PY33) and isinstance(item, ast.NameConstant):
+    elif (not PY2) and isinstance(item, ast.NameConstant):
         # None, True, False are nameconstants in python3, but names in 2
         return item.value
     else:
         return UnhandledKeyType()
 
 
+def is_notimplemented_name_node(node):
+    return isinstance(node, ast.Name) and getNodeName(node) == 'NotImplemented'
+
+
 class Binding(object):
     """
     Represents the binding of a value to a name.
@@ -410,8 +410,8 @@ class FunctionScope(Scope):
     @ivar globals: Names declared 'global' in this function.
     """
     usesLocals = False
-    alwaysUsed = set(['__tracebackhide__',
-                      '__traceback_info__', '__traceback_supplement__'])
+    alwaysUsed = {'__tracebackhide__', '__traceback_info__',
+                  '__traceback_supplement__'}
 
     def __init__(self):
         super(FunctionScope, self).__init__()
@@ -710,10 +710,14 @@ class Checker(object):
 
         # try enclosing function scopes and global scope
         for scope in self.scopeStack[-1::-1]:
-            # only generators used in a class scope can access the names
-            # of the class. this is skipped during the first iteration
-            if in_generators is False and isinstance(scope, ClassScope):
-                continue
+            if isinstance(scope, ClassScope):
+                if not PY2 and name == '__class__':
+                    return
+                elif in_generators is False:
+                    # only generators used in a class scope can access the
+                    # names of the class. this is skipped during the first
+                    # iteration
+                    continue
 
             try:
                 scope[name].used = (self.scope, node)
@@ -961,7 +965,7 @@ class Checker(object):
 
     # "stmt" type nodes
     DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
-        ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = EXEC = \
+        ASYNCWITH = ASYNCWITHITEM = TRYFINALLY = EXEC = \
         EXPR = ASSIGN = handleChildren
 
     PASS = ignore
@@ -985,6 +989,19 @@ class Checker(object):
         EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = \
         MATMULT = ignore
 
+    def RAISE(self, node):
+        self.handleChildren(node)
+
+        arg = get_raise_argument(node)
+
+        if isinstance(arg, ast.Call):
+            if is_notimplemented_name_node(arg.func):
+                # Handle "raise NotImplemented(...)"
+                self.report(messages.RaiseNotImplemented, node)
+        elif is_notimplemented_name_node(arg):
+            # Handle "raise NotImplemented"
+            self.report(messages.RaiseNotImplemented, node)
+
     # additional node types
     COMPREHENSION = KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildren
 
@@ -1178,9 +1195,9 @@ class Checker(object):
             wildcard = getattr(node.args, arg_name)
             if not wildcard:
                 continue
-            args.append(wildcard if PY33 else wildcard.arg)
+            args.append(wildcard if PY2 else wildcard.arg)
             if is_py3_func:
-                if PY33:  # Python 2.5 to 3.3
+                if PY2:  # Python 2.7
                     argannotation = arg_name + 'annotation'
                     annotations.append(getattr(node.args, argannotation))
                 else:     # Python >= 3.4
@@ -1221,7 +1238,7 @@ class Checker(object):
                     self.report(messages.UnusedVariable, binding.source, name)
             self.deferAssignment(checkUnusedAssignments)
 
-            if PY32:
+            if PY2:
                 def checkReturnWithArgumentInsideGenerator():
                     """
                     Check to see if there is any return statement with
diff --git a/pyflakes/messages.py b/pyflakes/messages.py
index 670f95f..eb87a72 100644
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -239,3 +239,7 @@ class ForwardAnnotationSyntaxError(Message):
     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'"
diff --git a/setup.py b/setup.py
index 387b7bc..6567b53 100755
--- a/setup.py
+++ b/setup.py
@@ -44,6 +44,7 @@ setup(
     author_email="code-quality@python.org",
     url="https://github.com/PyCQA/pyflakes",
     packages=["pyflakes", "pyflakes.scripts", "pyflakes.test"],
+    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
     classifiers=[
         "Development Status :: 6 - Mature",
         "Environment :: Console",
@@ -51,7 +52,13 @@ setup(
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python",
         "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: Implementation :: CPython",
+        "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: Software Development",
         "Topic :: Utilities",
     ],
diff --git a/tox.ini b/tox.ini
index 737e334..fabad9d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 skip_missing_interpreters = True
 envlist =
-    py26,py27,py33,py34,py35,py36,pypy,pypy3
+    py27,py34,py35,py36,pypy,pypy3
 
 [testenv]
 deps =


Test patch:
diff --git a/pyflakes/test/harness.py b/pyflakes/test/harness.py
index 009923f..3f592db 100644
--- a/pyflakes/test/harness.py
+++ b/pyflakes/test/harness.py
@@ -1,5 +1,4 @@
 
-import sys
 import textwrap
 import unittest
 
@@ -7,12 +6,8 @@ from pyflakes import checker
 
 __all__ = ['TestCase', 'skip', 'skipIf']
 
-if sys.version_info < (2, 7):
-    skip = lambda why: (lambda func: 'skip')  # not callable
-    skipIf = lambda cond, why: (skip(why) if cond else lambda func: func)
-else:
-    skip = unittest.skip
-    skipIf = unittest.skipIf
+skip = unittest.skip
+skipIf = unittest.skipIf
 PyCF_ONLY_AST = 1024
 
 
diff --git a/pyflakes/test/test_api.py b/pyflakes/test/test_api.py
index 8aa6df9..56ca269 100644
--- a/pyflakes/test/test_api.py
+++ b/pyflakes/test/test_api.py
@@ -180,6 +180,7 @@ class TestIterSourceCode(TestCase):
         """
         os.mkdir(os.path.join(self.tempdir, 'foo'))
         apath = self.makeEmptyFile('foo', 'a.py')
+        self.makeEmptyFile('foo', 'a.py~')
         os.mkdir(os.path.join(self.tempdir, 'bar'))
         bpath = self.makeEmptyFile('bar', 'b.py')
         cpath = self.makeEmptyFile('c.py')
@@ -514,7 +515,7 @@ foo(bar=baz, bax)
 foo(bar=baz, bax)
 %s""" % (sourcePath, column, message, last_line)])
 
-    @skipIf(PYPY, 'Output in PyPy varies highly, dependending on version')
+    @skipIf(PYPY, 'Output in PyPy varies highly, depending on version')
     def test_invalidEscape(self):
         """
         The invalid escape syntax raises ValueError in Python 2
diff --git a/pyflakes/test/test_dict.py b/pyflakes/test/test_dict.py
index 628ec0c..b9059c2 100644
--- a/pyflakes/test/test_dict.py
+++ b/pyflakes/test/test_dict.py
@@ -19,15 +19,11 @@ class Test(TestCase):
 
     @skipIf(version_info < (3,),
             "bytes and strings with same 'value' are not equal in python3")
-    @skipIf(version_info[0:2] == (3, 2),
-            "python3.2 does not allow u"" literal string definition")
     def test_duplicate_keys_bytes_vs_unicode_py3(self):
         self.flakes("{b'a': 1, u'a': 2}")
 
     @skipIf(version_info < (3,),
             "bytes and strings with same 'value' are not equal in python3")
-    @skipIf(version_info[0:2] == (3, 2),
-            "python3.2 does not allow u"" literal string definition")
     def test_duplicate_values_bytes_vs_unicode_py3(self):
         self.flakes(
             "{1: b'a', 1: u'a'}",
diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py
index 4723954..586a0a5 100644
--- a/pyflakes/test/test_imports.py
+++ b/pyflakes/test/test_imports.py
@@ -569,7 +569,6 @@ class Test(TestCase):
         ''')
 
     def test_redefinedByExcept(self):
-        as_exc = ', ' if version_info < (2, 6) else ' as '
         expected = [m.RedefinedWhileUnused]
         if version_info >= (3,):
             # The exc variable is unused inside the exception handler.
@@ -577,8 +576,8 @@ class Test(TestCase):
         self.flakes('''
         import fu
         try: pass
-        except Exception%sfu: pass
-        ''' % as_exc, *expected)
+        except Exception as fu: pass
+        ''', *expected)
 
     def test_usedInRaise(self):
         self.flakes('''
@@ -1149,13 +1148,6 @@ class TestSpecialAll(TestCase):
             return "hello"
         ''', m.UndefinedName)
 
-
-class Python26Tests(TestCase):
-    """
-    Tests for checking of syntax which is valid in Python 2.6 and newer.
-    """
-
-    @skipIf(version_info < (2, 6), "Python >= 2.6 only")
     def test_usedAsClassDecorator(self):
         """
         Using an imported name as a class decorator results in no warnings,
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 5bae7a0..364b375 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -81,7 +81,6 @@ class Test(TestCase):
         (1 for a, b in [(1, 2)])
         ''')
 
-    @skipIf(version_info < (2, 7), "Python >= 2.7 only")
     def test_redefinedInSetComprehension(self):
         """
         Test that reusing a variable in a set comprehension does not raise
@@ -111,7 +110,6 @@ class Test(TestCase):
         {1 for a, b in [(1, 2)]}
         ''')
 
-    @skipIf(version_info < (2, 7), "Python >= 2.7 only")
     def test_redefinedInDictComprehension(self):
         """
         Test that reusing a variable in a dict comprehension does not raise
@@ -279,7 +277,6 @@ class Test(TestCase):
             a = classmethod(a)
         ''')
 
-    @skipIf(version_info < (2, 6), "Python >= 2.6 only")
     def test_modernProperty(self):
         self.flakes("""
         class A:
@@ -1584,7 +1581,6 @@ class TestUnusedAssignment(TestCase):
             pass
         ''', m.UndefinedName)
 
-    @skipIf(version_info < (2, 7), "Python >= 2.7 only")
     def test_dictComprehension(self):
         """
         Dict comprehensions are properly handled.
@@ -1593,7 +1589,6 @@ class TestUnusedAssignment(TestCase):
         a = {1: x for x in range(10)}
         ''')
 
-    @skipIf(version_info < (2, 7), "Python >= 2.7 only")
     def test_setComprehensionAndLiteral(self):
         """
         Set comprehensions are properly handled.
@@ -1604,17 +1599,16 @@ class TestUnusedAssignment(TestCase):
         ''')
 
     def test_exceptionUsedInExcept(self):
-        as_exc = ', ' if version_info < (2, 6) else ' as '
         self.flakes('''
         try: pass
-        except Exception%se: e
-        ''' % as_exc)
+        except Exception as e: e
+        ''')
 
         self.flakes('''
         def download_review():
             try: pass
-            except Exception%se: e
-        ''' % as_exc)
+            except Exception as e: e
+        ''')
 
     @skipIf(version_info < (3,),
             "In Python 2 exception names stay bound after the exception handler")
@@ -1625,12 +1619,11 @@ class TestUnusedAssignment(TestCase):
         ''', m.UnusedVariable)
 
     def test_exceptionUnusedInExceptInFunction(self):
-        as_exc = ', ' if version_info < (2, 6) else ' as '
         self.flakes('''
         def download_review():
             try: pass
-            except Exception%se: pass
-        ''' % as_exc, m.UnusedVariable)
+            except Exception as e: pass
+        ''', m.UnusedVariable)
 
     def test_exceptWithoutNameInFunction(self):
         """
@@ -1983,3 +1976,20 @@ class TestAsyncStatements(TestCase):
         self.flakes('''
         a: 'a: "A"'
         ''', m.ForwardAnnotationSyntaxError)
+
+    def test_raise_notimplemented(self):
+        self.flakes('''
+        raise NotImplementedError("This is fine")
+        ''')
+
+        self.flakes('''
+        raise NotImplementedError
+        ''')
+
+        self.flakes('''
+        raise NotImplemented("This isn't gonna work")
+        ''', m.RaiseNotImplemented)
+
+        self.flakes('''
+        raise NotImplemented
+        ''', m.RaiseNotImplemented)
diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py
index 3d19210..8434cd8 100644
--- a/pyflakes/test/test_undefined_names.py
+++ b/pyflakes/test/test_undefined_names.py
@@ -728,14 +728,13 @@ class Test(TestCase):
             B = dict((i, str(i)) for i in T)
         ''')
 
-        if version_info >= (2, 7):
-            self.flakes('''
-            class A:
-                T = range(10)
+        self.flakes('''
+        class A:
+            T = range(10)
 
-                X = {x for x in T}
-                Y = {x:x for x in T}
-            ''')
+            X = {x for x in T}
+            Y = {x:x for x in T}
+        ''')
 
     def test_definedInClassNested(self):
         """Defined name for nested generator expressions in a class."""
@@ -761,7 +760,6 @@ class Test(TestCase):
         (42 for i in range(i))
         ''', m.UndefinedName)
 
-    @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist')
     def test_definedFromLambdaInDictionaryComprehension(self):
         """
         Defined name referenced from a lambda function within a dict/set
@@ -780,7 +778,6 @@ class Test(TestCase):
         any(lambda: id(x) for x in range(10))
         ''')
 
-    @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist')
     def test_undefinedFromLambdaInDictionaryComprehension(self):
         """
         Undefined name referenced from a lambda function within a dict/set
@@ -799,6 +796,24 @@ class Test(TestCase):
         any(lambda: id(y) for x in range(10))
         ''', m.UndefinedName)
 
+    def test_dunderClass(self):
+        """
+        `__class__` is defined in class scope under Python 3, but is not
+        in Python 2.
+        """
+        code = '''
+        class Test(object):
+            def __init__(self):
+                print(__class__.__name__)
+                self.x = 1
+
+        t = Test()
+        '''
+        if version_info < (3,):
+            self.flakes(code, m.UndefinedName)
+        else:
+            self.flakes(code)
+
 
 class NameTests(TestCase):
     """