Repository: PyCQA/pyflakes (python)

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).

Pull request description:
Drop support for EOL Python <2.7 and 3.2-3.3
Fixes #319.

This builds on #303, drops EOL versions and does some cleanup. It also adds `python_requires` to setup.py to help pip.

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):
     """