From 9580f88a9acfc8705003a55cb415141a6a0c02e8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 3 Aug 2018 17:38:41 +0700 Subject: [PATCH 1/2] Fix del builtin regression An exception to the handling of builtins is they can not be deleted. --- pyflakes/checker.py | 8 +++++--- pyflakes/test/test_undefined_names.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index c2f8d2e8..803f8227 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -871,10 +871,12 @@ def on_conditional_branch(): if isinstance(self.scope, FunctionScope) and name in self.scope.globals: self.scope.globals.remove(name) else: - try: - del self.scope[name] - except KeyError: + binding = self.scope.get(name, None) + if not binding or isinstance(binding, Builtin): self.report(messages.UndefinedName, node, name) + return + + del self.scope[name] def handleChildren(self, tree, omit=None): for node in iter_child_nodes(tree, omit=omit): diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py index 88eba933..9f86afde 100644 --- a/pyflakes/test/test_undefined_names.py +++ b/pyflakes/test/test_undefined_names.py @@ -371,6 +371,19 @@ def test_delUndefined(self): """Del an undefined name.""" self.flakes('del a', m.UndefinedName) + def test_del_builtin(self): + """Del a builtin not possible.""" + self.flakes('del range', m.UndefinedName) + + def test_del_shadowed_builtin(self): + """ + Del a function with same name as builtin. + """ + self.flakes(''' + def range(): pass + del range + ''') + def test_delConditional(self): """ Ignores conditional bindings deletion. From 128c845152e75695d1b2f099bed4ac7a804e8577 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 3 Aug 2018 22:35:06 +0700 Subject: [PATCH 2/2] Allow some builtins to be deleted __builtins__, __file__ and __debug__ can be deleted. Adds a test runner that ensures defined builtins can be loaded, and can be deleted when the python runtime allows it. --- pyflakes/checker.py | 10 ++++++- pyflakes/test/harness.py | 10 +++++++ pyflakes/test/test_builtin.py | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 803f8227..a4803dfe 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -185,6 +185,13 @@ class Builtin(Definition): def __init__(self, name): super(Builtin, self).__init__(name, None) + self.can_delete = False + # __builtins__ and __file__ can always be deleted. + # __debug__ can be deleted sometimes and not deleted other times. + # Safest course of action is to assume it can be deleted, in + # order that no error is reported by pyflakes + elif name in ('__builtins__', '__file__', '__debug__'): + self.can_delete = True def __repr__(self): return '<%s object %r at 0x%x>' % (self.__class__.__name__, @@ -872,7 +879,8 @@ def on_conditional_branch(): self.scope.globals.remove(name) else: binding = self.scope.get(name, None) - if not binding or isinstance(binding, Builtin): + if not binding or ( + isinstance(binding, Builtin) and not binding.can_delete): self.report(messages.UndefinedName, node, name) return diff --git a/pyflakes/test/harness.py b/pyflakes/test/harness.py index 0a58bd58..4a350cd3 100644 --- a/pyflakes/test/harness.py +++ b/pyflakes/test/harness.py @@ -15,6 +15,16 @@ class TestCase(unittest.TestCase): withDoctest = False + def pythonException(self, input, *expectedOutputs, **kw): + try: + compile(textwrap.dedent(input), '', 'exec', PyCF_ONLY_AST) + except BaseException as e: + return e + try: + exec(textwrap.dedent(input), {}) + except BaseException as e: + return e + def flakes(self, input, *expectedOutputs, **kw): tree = compile(textwrap.dedent(input), "", "exec", PyCF_ONLY_AST) if kw.get('is_segment'): diff --git a/pyflakes/test/test_builtin.py b/pyflakes/test/test_builtin.py index 7150ddb1..58ce822d 100644 --- a/pyflakes/test/test_builtin.py +++ b/pyflakes/test/test_builtin.py @@ -4,8 +4,15 @@ from sys import version_info from pyflakes import messages as m +from pyflakes.checker import Checker from pyflakes.test.harness import TestCase, skipIf +try: + WindowsError + WIN = True +except NameError: + WIN = False + class TestBuiltins(TestCase): @@ -39,3 +46,51 @@ def f(): f() ''', m.UndefinedLocal) + + +class TestLiveBuiltins(TestCase): + + def test_exists(self): + for name in sorted(Checker.builtIns): + # __file__ does exist in this test harness + if name == '__file__': + continue + + if name == 'WindowsError' and not WIN: + continue + + source = ''' + %s + ''' % name + e = self.pythonException(source) + self.assertIsNone(e) + + def test_del(self): + for name in sorted(Checker.builtIns): + # __file__ does exist in this test harness + if name == '__file__': + continue + + # __debug__ can be deleted sometimes and not deleted other times. + # Safest course of action is to assume it can be deleted, in + # order that no error is reported by pyflakes + if name == '__debug__': + continue + + source = ''' + del %s + ''' % name + + e = self.pythonException(source) + + if isinstance(e, SyntaxError): + if version_info < (3,): + # SyntaxError: invalid syntax + self.assertIn(name, ('print')) + else: + # SyntaxError: can't delete keyword + self.assertIn(name, ('None', 'True', 'False')) + elif isinstance(e, NameError): + self.flakes(source, m.UndefinedName) + else: + self.flakes(source)