From 60f5cadb236b10f72745a66946f870dfe97a688d Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 3 Aug 2018 22:35:06 +0700 Subject: [PATCH] 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 | 13 ++++++++- pyflakes/test/harness.py | 10 +++++++ pyflakes/test/test_builtin.py | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 7c67e75d..368b95d7 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -185,6 +185,16 @@ class Builtin(Definition): def __init__(self, name): super(Builtin, self).__init__(name, None) + self.can_delete = False + # __file__ can only be deleted on Python 3 + if not PY2 and name == '__file__': + self.can_delete = True + # __builtins__ 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__'): + self.can_delete = True def __repr__(self): return '<%s object %r at 0x%x>' % (self.__class__.__name__, @@ -865,7 +875,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)