Repository: PyCQA/pyflakes
Pull request #351 (2018-08-01 18:13:25)

checker.py: Handle UnboundLocal error for builtins
This patch ensures that UnboundLocalError is not
ignored for Python builtins. Populates ModuleScope
with Builtin nodes to manage them as bindings.

Fixes https://github.com/PyCQA/pyflakes/issues/350


Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 1c30dda..2ce6250 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -180,6 +180,18 @@ class Definition(Binding):
     """
 
 
+class Builtin(Definition):
+    """A definition created for all Python builtins."""
+
+    def __init__(self, name):
+        super(Builtin, self).__init__(name, None)
+
+    def __repr__(self):
+        return '<%s object %r at 0x%x>' % (self.__class__.__name__,
+                                           self.name,
+                                           id(self))
+
+
 class UnhandledKeyType(object):
     """
     A dictionary key of a type that we cannot or do not check for duplicates.
@@ -516,6 +528,8 @@ class Checker(object):
             raise RuntimeError('No scope implemented for the node %r' % tree)
         self.exceptHandlers = [()]
         self.root = tree
+        for builtin in self.builtIns:
+            self.addBinding(None, Builtin(builtin))
         self.handleChildren(tree)
         self.runDeferred(self._deferredFunctions)
         # Set _deferredFunctions to None so that deferFunction will fail
@@ -699,7 +713,8 @@ class Checker(object):
                 break
         existing = scope.get(value.name)
 
-        if existing and not self.differentForks(node, existing.source):
+        if (existing and not isinstance(existing, Builtin) and
+                not self.differentForks(node, existing.source)):
 
             parent_stmt = self.getParent(value.source)
             if isinstance(existing, Importation) and isinstance(parent_stmt, ast.For):
@@ -765,10 +780,6 @@ class Checker(object):
             if in_generators is not False:
                 in_generators = isinstance(scope, GeneratorScope)
 
-        # look in the built-ins
-        if name in self.builtIns:
-            return
-
         if importStarred:
             from_list = []
 
@@ -943,9 +954,7 @@ class Checker(object):
         self.scopeStack = [self.scopeStack[0]]
         node_offset = self.offset or (0, 0)
         self.pushScope(DoctestScope)
-        underscore_in_builtins = '_' in self.builtIns
-        if not underscore_in_builtins:
-            self.builtIns.add('_')
+        self.addBinding(None, Builtin('_'))
         for example in examples:
             try:
                 tree = compile(example.source, "<doctest>", "exec", ast.PyCF_ONLY_AST)
@@ -961,8 +970,6 @@ class Checker(object):
                                node_offset[1] + example.indent + 4)
                 self.handleChildren(tree)
                 self.offset = node_offset
-        if not underscore_in_builtins:
-            self.builtIns.remove('_')
         self.popScope()
         self.scopeStack = saved_stack
 
diff --git a/pyflakes/messages.py b/pyflakes/messages.py
index eb87a72..bd70693 100644
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -100,12 +100,19 @@ class UndefinedExport(Message):
 
 
 class UndefinedLocal(Message):
-    message = ('local variable %r (defined in enclosing scope on line %r) '
-               'referenced before assignment')
+    message = 'local variable %r {0} referenced before assignment'
+
+    default = 'defined in enclosing scope on line %r'
+    builtin = 'defined as a builtin'
 
     def __init__(self, filename, loc, name, orig_loc):
         Message.__init__(self, filename, loc)
-        self.message_args = (name, orig_loc.lineno)
+        if orig_loc is None:
+            self.message = self.message.format(self.builtin)
+            self.message_args = name
+        else:
+            self.message = self.message.format(self.default)
+            self.message_args = (name, orig_loc.lineno)
 
 
 class DuplicateArgument(Message):


Test patch:
diff --git a/pyflakes/test/test_builtin.py b/pyflakes/test/test_builtin.py
new file mode 100644
index 0000000..7150ddb
--- /dev/null
+++ b/pyflakes/test/test_builtin.py
@@ -0,0 +1,41 @@
+"""
+Tests for detecting redefinition of builtins.
+"""
+from sys import version_info
+
+from pyflakes import messages as m
+from pyflakes.test.harness import TestCase, skipIf
+
+
+class TestBuiltins(TestCase):
+
+    def test_builtin_unbound_local(self):
+        self.flakes('''
+        def foo():
+            a = range(1, 10)
+            range = a
+            return range
+
+        foo()
+
+        print(range)
+        ''', m.UndefinedLocal)
+
+    def test_global_shadowing_builtin(self):
+        self.flakes('''
+        def f():
+            global range
+            range = None
+            print(range)
+
+        f()
+        ''')
+
+    @skipIf(version_info >= (3,), 'not an UnboundLocalError in Python 3')
+    def test_builtin_in_comprehension(self):
+        self.flakes('''
+        def f():
+            [range for range in range(1, 10)]
+
+        f()
+        ''', m.UndefinedLocal)